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

@ -13,10 +13,15 @@ import {
import { AnimatePresence, motion, type Variants } from "motion/react"; import { AnimatePresence, motion, type Variants } from "motion/react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -49,9 +54,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { cn } from "@/lib/utils"; import { useAtom, useAtomValue } from "jotai";
import { useAtomValue } from "jotai"; import { activeSearchSpaceChatsAtom } from "@/atoms/chats/queries/active-search-space-chats.query.atom";
import { activeSearchSpaceChatsAtom } from "@/atoms/chats/queries/active-search-space-chats.atom"; import { deleteChatMutationAtom } from "@/atoms/chats/mutations/delete-chat.mutation.atom";
export interface Chat { export interface Chat {
created_at: string; created_at: string;
@ -91,7 +96,9 @@ const chatCardVariants: Variants = {
const MotionCard = motion(Card); const MotionCard = motion(Card);
export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) { export default function ChatsPageClient({
searchSpaceId,
}: ChatsPageClientProps) {
const router = useRouter(); const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]); const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -100,9 +107,18 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
const [selectedType, setSelectedType] = useState<string>("all"); const [selectedType, setSelectedType] = useState<string>("all");
const [sortOrder, setSortOrder] = useState<string>("newest"); const [sortOrder, setSortOrder] = useState<string>("newest");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; title: string } | null>(null); const [chatToDelete, setChatToDelete] = useState<{
const [isDeleting, setIsDeleting] = useState(false); id: number;
const {isFetching , data : chats, error} = useAtomValue(activeSearchSpaceChatsAtom); 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 chatsPerPage = 9;
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -118,12 +134,17 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
} }
}, [searchParams]); }, [searchParams]);
useEffect(() => {
if (fetchError) {
console.error("Error fetching chats:", fetchError);
}
}, [fetchError]);
useEffect(() => { useEffect(() => {
if (error) { if (deleteError) {
console.error("Error fetching chats:", error); console.error("Error deleting chat:", deleteError);
} }
}, [error]); }, [deleteError]);
// Filter and sort chats based on search query, type, and sort order // Filter and sort chats based on search query, type, and sort order
useEffect(() => { useEffect(() => {
@ -132,7 +153,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
// Filter by search term // Filter by search term
if (searchQuery) { if (searchQuery) {
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
result = result.filter((chat) => chat.title.toLowerCase().includes(query)); result = result.filter((chat) =>
chat.title.toLowerCase().includes(query)
);
} }
// Filter by type // Filter by type
@ -152,49 +175,22 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage))); setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
// Reset to first page when filters change // Reset to first page when filters change
if (currentPage !== 1 && (searchQuery || selectedType !== "all" || sortOrder !== "newest")) { if (
currentPage !== 1 &&
(searchQuery || selectedType !== "all" || sortOrder !== "newest")
) {
setCurrentPage(1); setCurrentPage(1);
} }
}, [chats, searchQuery, selectedType, sortOrder, currentPage]); }, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Function to handle chat deletion // Function to handle chat deletion
const handleDeleteChat = async () => { const handleDeleteChat = async () => {
// if (!chatToDelete) return; if (!chatToDelete) return;
// setIsDeleting(true); await deleteChat(chatToDelete.id);
// try {
// const token = localStorage.getItem("surfsense_bearer_token");
// if (!token) {
// setIsDeleting(false);
// return;
// }
// const response = await fetch( setDeleteDialogOpen(false);
// `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`, setChatToDelete(null);
// {
// method: "DELETE",
// headers: {
// Authorization: `Bearer ${token}`,
// "Content-Type": "application/json",
// },
// }
// );
// if (!response.ok) {
// throw new Error(`Failed to delete chat: ${response.statusText}`);
// }
// // Close dialog and refresh chats
// setDeleteDialogOpen(false);
// setChatToDelete(null);
// // 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);
// }
}; };
// Calculate pagination // Calculate pagination
@ -203,7 +199,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
// Get unique chat types for filter dropdown // Get unique chat types for filter dropdown
const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : []; const chatTypes = chats
? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]
: [];
return ( return (
<motion.div <motion.div
@ -216,7 +214,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<div className="flex flex-col space-y-4 md:space-y-6"> <div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">All Chats</h1> <h1 className="text-3xl font-bold tracking-tight">All Chats</h1>
<p className="text-muted-foreground">View, search, and manage all your chats.</p> <p className="text-muted-foreground">
View, search, and manage all your chats.
</p>
</div> </div>
{/* Filter and Search Bar */} {/* Filter and Search Bar */}
@ -241,7 +241,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<SelectGroup> <SelectGroup>
{chatTypes.map((type) => ( {chatTypes.map((type) => (
<SelectItem key={type} value={type}> <SelectItem key={type} value={type}>
{type === "all" ? "All Types" : type.charAt(0).toUpperCase() + type.slice(1)} {type === "all"
? "All Types"
: type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem> </SelectItem>
))} ))}
</SelectGroup> </SelectGroup>
@ -265,7 +267,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</div> </div>
{/* Status Messages */} {/* Status Messages */}
{isFetching && ( {isFetchingChats && (
<div className="flex items-center justify-center h-40"> <div className="flex items-center justify-center h-40">
<div className="flex flex-col items-center gap-2"> <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> <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
@ -274,14 +276,14 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</div> </div>
)} )}
{error && !isFetching && ( {fetchError && !isFetchingChats && (
<div className="border border-destructive/50 text-destructive p-4 rounded-md"> <div className="border border-destructive/50 text-destructive p-4 rounded-md">
<h3 className="font-medium">Error loading chats</h3> <h3 className="font-medium">Error loading chats</h3>
<p className="text-sm">{error.message}</p> <p className="text-sm">{fetchError.message}</p>
</div> </div>
)} )}
{!isFetching && !error && filteredChats.length === 0 && ( {!isFetchingChats && !fetchError && filteredChats.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center"> <div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" /> <MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3> <h3 className="font-medium">No chats found</h3>
@ -294,7 +296,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
)} )}
{/* Chat Grid */} {/* Chat Grid */}
{!isFetching && !error && filteredChats.length > 0 && ( {!isFetchingChats && !fetchError && filteredChats.length > 0 && (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentChats.map((chat, index) => ( {currentChats.map((chat, index) => (
@ -316,13 +318,19 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<CardDescription> <CardDescription>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" /> <Calendar className="h-3.5 w-3.5" />
<span>{format(new Date(chat.created_at), "MMM d, yyyy")}</span> <span>
{format(new Date(chat.created_at), "MMM d, yyyy")}
</span>
</span> </span>
</CardDescription> </CardDescription>
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8"> <Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
</Button> </Button>
@ -366,7 +374,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<Button <Button
size="sm" size="sm"
onClick={() => onClick={() =>
router.push(`/dashboard/${chat.search_space_id}/researcher/${chat.id}`) router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
} }
> >
<MessageCircleMore className="h-4 w-4" /> <MessageCircleMore className="h-4 w-4" />
@ -380,7 +390,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
)} )}
{/* Pagination */} {/* Pagination */}
{!isFetching && !error && totalPages > 1 && ( {!isFetchingChats && !fetchError && totalPages > 1 && (
<Pagination className="mt-8"> <Pagination className="mt-8">
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
@ -390,7 +400,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
e.preventDefault(); e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1); if (currentPage > 1) setCurrentPage(currentPage - 1);
}} }}
className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""} className={
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
}
/> />
</PaginationItem> </PaginationItem>
@ -399,14 +411,17 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
const isVisible = const isVisible =
pageNumber === 1 || pageNumber === 1 ||
pageNumber === totalPages || pageNumber === totalPages ||
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1); (pageNumber >= currentPage - 1 &&
pageNumber <= currentPage + 1);
if (!isVisible) { if (!isVisible) {
// Show ellipsis at appropriate positions // Show ellipsis at appropriate positions
if (pageNumber === 2 || pageNumber === totalPages - 1) { if (pageNumber === 2 || pageNumber === totalPages - 1) {
return ( return (
<PaginationItem key={pageNumber}> <PaginationItem key={pageNumber}>
<span className="flex h-9 w-9 items-center justify-center">...</span> <span className="flex h-9 w-9 items-center justify-center">
...
</span>
</PaginationItem> </PaginationItem>
); );
} }
@ -434,9 +449,14 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
href={`?page=${Math.min(totalPages, currentPage + 1)}`} href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1); if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}} }}
className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} className={
currentPage >= totalPages
? "pointer-events-none opacity-50"
: ""
}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
@ -454,25 +474,25 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete{" "} Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This action cannot be <span className="font-medium">{chatToDelete?.title}</span>? This
undone. action cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end"> <DialogFooter className="flex gap-2 sm:justify-end">
<Button <Button
variant="outline" variant="outline"
onClick={() => setDeleteDialogOpen(false)} onClick={() => setDeleteDialogOpen(false)}
disabled={isDeleting} disabled={isDeletingChat}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
onClick={handleDeleteChat} onClick={handleDeleteChat}
disabled={isDeleting} disabled={isDeletingChat}
className="gap-2" className="gap-2"
> >
{isDeleting ? ( {isDeletingChat ? (
<> <>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting... Deleting...

View file

@ -27,8 +27,8 @@ import {
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useLLMPreferences } from "@/hooks/use-llm-configs"; import { useLLMPreferences } from "@/hooks/use-llm-configs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { activeChatIdAtom } from "@/atoms/chats/queries/active-chat.atom"; import { activeChatIdAtom } from "@/atoms/chats/queries/active-chat.query.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat-ui.atom"; import { chatUIAtom } from "@/atoms/chats/active-chat.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/active-seach-space.atom"; import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/active-seach-space.atom";
export function DashboardClientLayout({ 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 activeChatId = get(activeChatIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token"); const authToken = localStorage.getItem("surfsense_bearer_token");
if (!activeChatId) {
return { return {
queryKey: [], queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""),
enabled: false,
queryFn: async () => {
return { chatId: null, chatDetails: null, podcast: null };
},
};
}
return {
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId),
enabled: !!activeChatId && !!authToken, enabled: !!activeChatId && !!authToken,
queryFn: async () => { queryFn: async () => {
if (!authToken) { if (!authToken) {

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { useAtomValue } from "jotai";
import { Pencil } from "lucide-react"; import { Pencil } from "lucide-react";
import { useCallback, useContext, useState } from "react"; import { useCallback, useContext, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 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"; import type { GeneratePodcastRequest } from "./ChatPanelContainer";
interface ConfigModalProps { 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}`); throw new Error(`Failed to delete chat: ${response.statusText}`);
} }
return true;
} catch (err) { } catch (err) {
console.error("Error deleting chat:", err); console.error("Error deleting chat:", err);
return false;
} }
}; };

View file

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