SurfSense/surfsense_web/components/sidebar/AppSidebarProvider.tsx

336 lines
8.7 KiB
TypeScript
Raw Normal View History

2025-07-27 10:05:37 -07:00
"use client";
2025-04-07 23:47:06 -07:00
2025-11-18 22:12:47 +02:00
import { useAtom, useAtomValue, useSetAtom } from "jotai";
2025-08-08 11:17:43 -07:00
import { Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
2025-11-18 22:12:47 +02:00
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
2025-11-19 08:29:33 +02:00
import { chatsAtom } from "@/atoms/chats/chat-query.atoms";
2025-11-18 22:12:47 +02:00
import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms";
2025-07-27 10:05:37 -07:00
import { AppSidebar } from "@/components/sidebar/app-sidebar";
2025-07-27 10:41:15 -07:00
import { Button } from "@/components/ui/button";
2025-04-07 23:47:06 -07:00
import {
2025-07-27 10:05:37 -07:00
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
2025-04-07 23:47:06 -07:00
} from "@/components/ui/dialog";
import { useQuery } from "@tanstack/react-query";
import { notesApiService } from "@/lib/apis/notes-api.service";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
2025-04-07 23:47:06 -07:00
interface AppSidebarProviderProps {
2025-07-27 10:05:37 -07:00
searchSpaceId: string;
navSecondary: {
title: string;
url: string;
icon: string;
}[];
navMain: {
title: string;
url: string;
icon: string;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
2025-04-07 23:47:06 -07:00
}
export function AppSidebarProvider({
2025-07-27 10:05:37 -07:00
searchSpaceId,
navSecondary,
navMain,
2025-04-07 23:47:06 -07:00
}: AppSidebarProviderProps) {
const t = useTranslations("dashboard");
const tCommon = useTranslations("common");
const router = useRouter();
2025-11-18 22:12:47 +02:00
const setChatsQueryParams = useSetAtom(globalChatsQueryParamsAtom);
const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom);
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
useAtom(deleteChatMutationAtom);
2025-11-18 22:12:47 +02:00
useEffect(() => {
setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 5 }));
2025-11-18 22:12:47 +02:00
}, [searchSpaceId]);
const {
data: searchSpace,
isLoading: isLoadingSearchSpace,
error: searchSpaceError,
refetch: fetchSearchSpace,
} = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }),
enabled: !!searchSpaceId,
});
const { data: user } = useAtomValue(currentUserAtom);
// Fetch notes
const {
data: notesData,
error: notesError,
isLoading: isLoadingNotes,
refetch: refetchNotes,
} = useQuery({
queryKey: ["notes", searchSpaceId],
queryFn: () =>
notesApiService.getNotes({
search_space_id: Number(searchSpaceId),
page_size: 5, // Get 5 notes (changed from 10)
}),
enabled: !!searchSpaceId,
});
2025-07-27 10:05:37 -07:00
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
const [isClient, setIsClient] = useState(false);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
// Set isClient to true when component mounts on the client
useEffect(() => {
setIsClient(true);
}, []);
2025-04-07 23:47:06 -07:00
2025-08-02 21:20:36 -07:00
// Retry function
const retryFetch = useCallback(() => {
fetchSearchSpace();
2025-11-18 22:12:47 +02:00
}, [fetchSearchSpace]);
2025-08-02 21:20:36 -07:00
// Transform API response to the format expected by AppSidebar
const recentChats = useMemo(() => {
2025-11-18 22:12:47 +02:00
return chats
? chats.map((chat) => ({
name: chat.title || `Chat ${chat.id}`,
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
icon: "MessageCircleMore",
id: chat.id,
search_space_id: chat.search_space_id,
actions: [
{
name: "Delete",
icon: "Trash2",
onClick: () => {
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
setShowDeleteDialog(true);
},
},
],
}))
: [];
}, [chats]);
2025-08-02 21:20:36 -07:00
// Handle delete chat with better error handling
const handleDeleteChat = useCallback(async () => {
2025-07-27 10:05:37 -07:00
if (!chatToDelete) return;
2025-07-27 10:05:37 -07:00
try {
2025-11-18 22:12:47 +02:00
await deleteChat({ id: chatToDelete.id });
2025-07-27 10:05:37 -07:00
} catch (error) {
console.error("Error deleting chat:", error);
2025-08-02 21:20:36 -07:00
// You could show a toast notification here
2025-07-27 10:05:37 -07:00
} finally {
setShowDeleteDialog(false);
setChatToDelete(null);
}
}, [chatToDelete, deleteChat]);
2025-08-02 21:20:36 -07:00
// Memoized fallback chats
const fallbackChats = useMemo(() => {
if (chatError) {
return [
{
name: t("error_loading_chats"),
2025-08-02 21:20:36 -07:00
url: "#",
icon: "AlertCircle",
id: 0,
search_space_id: Number(searchSpaceId),
actions: [
{
name: tCommon("retry"),
2025-08-02 21:20:36 -07:00
icon: "RefreshCw",
onClick: retryFetch,
},
],
},
];
}
2025-04-07 23:47:06 -07:00
2025-08-02 21:20:36 -07:00
if (!isLoadingChats && recentChats.length === 0) {
return [
{
name: t("no_recent_chats"),
2025-08-02 21:20:36 -07:00
url: "#",
icon: "MessageCircleMore",
id: 0,
search_space_id: Number(searchSpaceId),
actions: [],
},
];
}
2025-04-07 23:47:06 -07:00
2025-08-02 21:20:36 -07:00
return [];
}, [chatError, isLoadingChats, recentChats.length, searchSpaceId, retryFetch, t, tCommon]);
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
// Use fallback chats if there's an error or no chats
const displayChats = recentChats.length > 0 ? recentChats : fallbackChats;
2025-04-07 23:47:06 -07:00
// Transform notes to the format expected by NavNotes
const recentNotes = useMemo(() => {
if (!notesData?.items) return [];
// Sort notes by updated_at (most recent first), fallback to created_at if updated_at is null
const sortedNotes = [...notesData.items].sort((a, b) => {
const dateA = a.updated_at
? new Date(a.updated_at).getTime()
: new Date(a.created_at).getTime();
const dateB = b.updated_at
? new Date(b.updated_at).getTime()
: new Date(b.created_at).getTime();
return dateB - dateA; // Descending order (most recent first)
});
// Limit to 5 notes
return sortedNotes.slice(0, 5).map((note) => ({
name: note.title,
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
icon: "FileText",
id: note.id,
search_space_id: note.search_space_id,
actions: [
{
name: "Delete",
icon: "Trash2",
onClick: async () => {
try {
await notesApiService.deleteNote({
search_space_id: note.search_space_id,
note_id: note.id,
});
refetchNotes();
} catch (error) {
console.error("Error deleting note:", error);
}
},
},
],
}));
}, [notesData, refetchNotes]);
// Handle add note
const handleAddNote = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/editor/new`);
}, [router, searchSpaceId]);
2025-08-02 21:20:36 -07:00
// Memoized updated navSecondary
const updatedNavSecondary = useMemo(() => {
const updated = [...navSecondary];
if (updated.length > 0 && isClient) {
updated[0] = {
...updated[0],
title:
searchSpace?.name ||
(isLoadingSearchSpace
? tCommon("loading")
2025-08-02 21:20:36 -07:00
: searchSpaceError
? t("error_loading_space")
: t("unknown_search_space")),
2025-08-02 21:20:36 -07:00
};
}
return updated;
}, [
navSecondary,
isClient,
searchSpace?.name,
isLoadingSearchSpace,
searchSpaceError,
t,
tCommon,
]);
2025-08-02 21:20:36 -07:00
// Prepare page usage data
const pageUsage = user
? {
pagesUsed: user.pages_used,
pagesLimit: user.pages_limit,
}
: undefined;
2025-08-02 21:20:36 -07:00
// Show loading state if not client-side
if (!isClient) {
return (
<AppSidebar
searchSpaceId={searchSpaceId}
navSecondary={navSecondary}
navMain={navMain}
RecentChats={[]}
RecentNotes={[]}
pageUsage={pageUsage}
/>
);
2025-07-27 10:05:37 -07:00
}
2025-04-07 23:47:06 -07:00
2025-07-27 10:05:37 -07:00
return (
<>
<AppSidebar
searchSpaceId={searchSpaceId}
navSecondary={updatedNavSecondary}
navMain={navMain}
RecentChats={displayChats}
RecentNotes={recentNotes}
onAddNote={handleAddNote}
pageUsage={pageUsage}
/>
2025-08-02 21:20:36 -07:00
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>{t("delete_chat")}</span>
2025-08-02 21:20:36 -07:00
</DialogTitle>
<DialogDescription>
{t("delete_chat_confirm")} <span className="font-medium">{chatToDelete?.name}</span>?{" "}
{t("action_cannot_undone")}
2025-08-02 21:20:36 -07:00
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setShowDeleteDialog(false)}
2025-11-18 22:12:47 +02:00
disabled={isDeletingChat}
2025-08-02 21:20:36 -07:00
>
{tCommon("cancel")}
2025-08-02 21:20:36 -07:00
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
2025-11-18 22:12:47 +02:00
disabled={isDeletingChat}
2025-08-02 21:20:36 -07:00
className="gap-2"
>
2025-11-18 22:12:47 +02:00
{isDeletingChat ? (
2025-08-02 21:20:36 -07:00
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("deleting")}
2025-08-02 21:20:36 -07:00
</>
) : (
<>
<Trash2 className="h-4 w-4" />
{tCommon("delete")}
2025-08-02 21:20:36 -07:00
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
2025-07-27 10:05:37 -07:00
</>
);
}