Merge pull request #618 from MODSetter/dev

feat: removed All chats,notes sidebar sheet
This commit is contained in:
Rohan Verma 2025-12-23 20:52:05 -08:00 committed by GitHub
commit 18e55a7ed7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 485 additions and 369 deletions

View file

@ -12,9 +12,11 @@ import {
Trash2, Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -25,14 +27,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { import {
@ -58,10 +52,39 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null); const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [showArchived, setShowArchived] = useState(false); const [showArchived, setShowArchived] = useState(false);
const [mounted, setMounted] = useState(false);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
const isSearchMode = !!debouncedSearchQuery.trim(); const isSearchMode = !!debouncedSearchQuery.trim();
// Handle mounting for portal
useEffect(() => {
setMounted(true);
}, []);
// Handle escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
onOpenChange(false);
}
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [open, onOpenChange]);
// Lock body scroll when open
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [open]);
// Fetch all threads (when not searching) // Fetch all threads (when not searching)
const { const {
data: threadsData, data: threadsData,
@ -100,7 +123,6 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
try { try {
await deleteThread(threadId); await deleteThread(threadId);
toast.success(t("chat_deleted") || "Chat deleted successfully"); toast.success(t("chat_deleted") || "Chat deleted successfully");
// Invalidate queries to refresh the list
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
@ -158,24 +180,58 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
const activeCount = threadsData?.threads.length ?? 0; const activeCount = threadsData?.threads.length ?? 0;
const archivedCount = threadsData?.archived_threads.length ?? 0; const archivedCount = threadsData?.archived_threads.length ?? 0;
return ( if (!mounted) return null;
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col border-0"> return createPortal(
<SheetHeader className="mx-3 px-4 pt-4 pb-0 space-y-2"> <AnimatePresence>
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle> {open && (
<SheetDescription className="sr-only"> <>
{t("all_chats_description") || "Browse and manage all your chats"} {/* Backdrop */}
</SheetDescription> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 bg-black/50"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
{/* Panel */}
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
role="dialog"
aria-modal="true"
aria-label={t("all_chats") || "All Chats"}
>
{/* Header */}
<div className="flex-shrink-0 p-4 pb-2 space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t("all_chats") || "All Chats"}</h2>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
{/* Search Input */} {/* Search Input */}
<div className="relative"> <div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t("search_chats") || "Search chats..."} placeholder={t("search_chats") || "Search chats..."}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none" className="pl-9 pr-8 h-9"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
@ -189,11 +245,11 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
</Button> </Button>
)} )}
</div> </div>
</SheetHeader> </div>
{/* Tab toggle for active/archived (only show when not searching) */} {/* Tab toggle for active/archived (only show when not searching) */}
{!isSearchMode && ( {!isSearchMode && (
<div className="flex border-b mx-3 -mt-3"> <div className="flex-shrink-0 flex border-b mx-4">
<button <button
type="button" type="button"
onClick={() => setShowArchived(false)} onClick={() => setShowArchived(false)}
@ -221,8 +277,8 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
</div> </div>
)} )}
<ScrollArea className="flex-1 min-h-0 overflow-hidden"> {/* Scrollable Content */}
<div className="p-2"> <div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@ -255,13 +311,13 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
type="button" type="button"
onClick={() => handleThreadClick(thread.id)} onClick={() => handleThreadClick(thread.id)}
disabled={isBusy} disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left" className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
> >
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" /> <MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span> <span className="truncate">{thread.title || "New Chat"}</span>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="bottom" align="start">
<p> <p>
{t("updated") || "Updated"}:{" "} {t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")} {format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
@ -347,8 +403,10 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
</div> </div>
)} )}
</div> </div>
</ScrollArea> </motion.div>
</SheetContent> </>
</Sheet> )}
</AnimatePresence>,
document.body
); );
} }

View file

@ -3,9 +3,11 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns"; import { format } from "date-fns";
import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react"; import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -14,14 +16,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { documentsApiService } from "@/lib/apis/documents-api.service"; import { documentsApiService } from "@/lib/apis/documents-api.service";
@ -46,8 +40,37 @@ export function AllNotesSidebar({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null); const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [mounted, setMounted] = useState(false);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
// Handle mounting for portal
useEffect(() => {
setMounted(true);
}, []);
// Handle escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
onOpenChange(false);
}
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [open, onOpenChange]);
// Lock body scroll when open
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [open]);
// Fetch all notes (when no search query) // Fetch all notes (when no search query)
const { const {
data: notesData, data: notesData,
@ -100,7 +123,6 @@ export function AllNotesSidebar({
search_space_id: noteSearchSpaceId, search_space_id: noteSearchSpaceId,
note_id: noteId, note_id: noteId,
}); });
// Invalidate queries to refresh the list
queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] });
@ -157,24 +179,58 @@ export function AllNotesSidebar({
}); });
}, [isSearchMode, searchData, notesData]); }, [isSearchMode, searchData, notesData]);
return ( if (!mounted) return null;
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col border-0"> return createPortal(
<SheetHeader className="mx-3 px-4 pt-4 pb-2 border-b space-y-2"> <AnimatePresence>
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle> {open && (
<SheetDescription className="sr-only"> <>
{t("all_notes_description") || "Browse and manage all your notes"} {/* Backdrop */}
</SheetDescription> <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 bg-black/50"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
{/* Panel */}
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
role="dialog"
aria-modal="true"
aria-label={t("all_notes") || "All Notes"}
>
{/* Header */}
<div className="flex-shrink-0 p-4 pb-3 space-y-3 border-b">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t("all_notes") || "All Notes"}</h2>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
{/* Search Input */} {/* Search Input */}
<div className="relative"> <div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t("search_notes") || "Search notes..."} placeholder={t("search_notes") || "Search notes..."}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none" className="pl-9 pr-8 h-9"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
@ -188,10 +244,10 @@ export function AllNotesSidebar({
</Button> </Button>
)} )}
</div> </div>
</SheetHeader> </div>
<ScrollArea className="flex-1 min-h-0 overflow-hidden"> {/* Scrollable Content */}
<div className="p-2"> <div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@ -222,13 +278,13 @@ export function AllNotesSidebar({
type="button" type="button"
onClick={() => handleNoteClick(note.id, note.search_space_id)} onClick={() => handleNoteClick(note.id, note.search_space_id)}
disabled={isDeleting} disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left" className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
> >
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" /> <FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{note.title}</span> <span className="truncate">{note.title}</span>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="bottom" align="start">
<div className="space-y-1"> <div className="space-y-1">
<p> <p>
{t("created") || "Created"}:{" "} {t("created") || "Created"}:{" "}
@ -311,11 +367,10 @@ export function AllNotesSidebar({
</div> </div>
)} )}
</div> </div>
</ScrollArea>
{/* Footer with Add Note button */} {/* Footer with Add Note button */}
{onAddNote && notes.length > 0 && ( {onAddNote && notes.length > 0 && (
<div className="mx-3 p-3"> <div className="flex-shrink-0 p-3 border-t">
<Button <Button
onClick={() => { onClick={() => {
onAddNote(); onAddNote();
@ -329,7 +384,10 @@ export function AllNotesSidebar({
</Button> </Button>
</div> </div>
)} )}
</SheetContent> </motion.div>
</Sheet> </>
)}
</AnimatePresence>,
document.body
); );
} }