mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-08 23:32:40 +02:00
add delete chat mutation
This commit is contained in:
parent
bd4e5d627d
commit
b866170e6a
11 changed files with 486 additions and 445 deletions
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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!),
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue