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 { 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 {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
@ -49,9 +54,9 @@ import {
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;
@ -91,7 +96,9 @@ const chatCardVariants: Variants = {
const MotionCard = motion(Card);
export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) {
export default function ChatsPageClient({
searchSpaceId,
}: ChatsPageClientProps) {
const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState("");
@ -100,9 +107,18 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
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);
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();
@ -118,12 +134,17 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
}
}, [searchParams]);
useEffect(() => {
if (fetchError) {
console.error("Error fetching chats:", fetchError);
}
}, [fetchError]);
useEffect(() => {
if (error) {
console.error("Error fetching chats:", error);
if (deleteError) {
console.error("Error deleting chat:", deleteError);
}
}, [error]);
}, [deleteError]);
// Filter and sort chats based on search query, type, and sort order
useEffect(() => {
@ -132,7 +153,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
// Filter by search term
if (searchQuery) {
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
@ -152,49 +175,22 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
// 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);
}
}, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Function to handle chat deletion
const handleDeleteChat = async () => {
// if (!chatToDelete) return;
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",
// },
// }
// );
// 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);
// }
setDeleteDialogOpen(false);
setChatToDelete(null);
};
// Calculate pagination
@ -203,7 +199,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
// 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 (
<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-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>
<p className="text-muted-foreground">
View, search, and manage all your chats.
</p>
</div>
{/* Filter and Search Bar */}
@ -241,7 +241,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<SelectGroup>
{chatTypes.map((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>
))}
</SelectGroup>
@ -265,7 +267,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</div>
{/* Status Messages */}
{isFetching && (
{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>
@ -274,14 +276,14 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</div>
)}
{error && !isFetching && (
{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">{error.message}</p>
<p className="text-sm">{fetchError.message}</p>
</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">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3>
@ -294,7 +296,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
)}
{/* Chat Grid */}
{!isFetching && !error && filteredChats.length > 0 && (
{!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) => (
@ -316,13 +318,19 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<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>
{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">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
@ -366,7 +374,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
<Button
size="sm"
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" />
@ -380,7 +390,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
)}
{/* Pagination */}
{!isFetching && !error && totalPages > 1 && (
{!isFetchingChats && !fetchError && totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
@ -390,7 +400,9 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""}
className={
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
@ -399,14 +411,17 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
const isVisible =
pageNumber === 1 ||
pageNumber === totalPages ||
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1);
(pageNumber >= currentPage - 1 &&
pageNumber <= currentPage + 1);
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>
<span className="flex h-9 w-9 items-center justify-center">
...
</span>
</PaginationItem>
);
}
@ -434,9 +449,14 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => {
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>
</PaginationContent>
@ -454,25 +474,25 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This action cannot be
undone.
<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}
disabled={isDeletingChat}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeleting}
disabled={isDeletingChat}
className="gap-2"
>
{isDeleting ? (
{isDeletingChat ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting...

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,
},
};