feat: enhance sidebar functionality with tooltips and improved sorting

- Added tooltips to chat and note items in the sidebars, displaying creation and update timestamps.
- Implemented sorting of chats and notes by their creation or update dates for better organization.
- Updated translation keys for new UI strings related to deletion and timestamps.
- Adjusted sidebar layout for improved user experience on mobile devices.
This commit is contained in:
Anish Sarkar 2025-12-19 21:40:40 +05:30
parent 60d57305a7
commit bb971f89ba
11 changed files with 293 additions and 113 deletions

View file

@ -91,6 +91,9 @@ export function AppSidebarProvider({
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
const [showDeleteNoteDialog, setShowDeleteNoteDialog] = useState(false);
const [noteToDelete, setNoteToDelete] = useState<{ id: number; name: string; search_space_id: number } | null>(null);
const [isDeletingNote, setIsDeletingNote] = useState(false);
const [isClient, setIsClient] = useState(false);
// Set isClient to true when component mounts on the client
@ -105,25 +108,32 @@ export function AppSidebarProvider({
// Transform API response to the format expected by AppSidebar
const recentChats = useMemo(() => {
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);
},
},
],
}))
: [];
if (!chats) return [];
// Sort chats by created_at (most recent first)
const sortedChats = [...chats].sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return dateB - dateA; // Descending order (most recent first)
});
return sortedChats.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]);
// Handle delete chat with better error handling
@ -141,6 +151,26 @@ export function AppSidebarProvider({
}
}, [chatToDelete, deleteChat]);
// Handle delete note with confirmation
const handleDeleteNote = useCallback(async () => {
if (!noteToDelete) return;
setIsDeletingNote(true);
try {
await notesApiService.deleteNote({
search_space_id: noteToDelete.search_space_id,
note_id: noteToDelete.id,
});
refetchNotes();
} catch (error) {
console.error("Error deleting note:", error);
} finally {
setIsDeletingNote(false);
setShowDeleteNoteDialog(false);
setNoteToDelete(null);
}
}, [noteToDelete, refetchNotes]);
// Memoized fallback chats
const fallbackChats = useMemo(() => {
if (chatError) {
@ -162,19 +192,6 @@ export function AppSidebarProvider({
];
}
if (!isLoadingChats && recentChats.length === 0) {
return [
{
name: t("no_recent_chats"),
url: "#",
icon: "MessageCircleMore",
id: 0,
search_space_id: Number(searchSpaceId),
actions: [],
},
];
}
return [];
}, [chatError, isLoadingChats, recentChats.length, searchSpaceId, retryFetch, t, tCommon]);
@ -207,21 +224,14 @@ export function AppSidebarProvider({
{
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);
}
onClick: () => {
setNoteToDelete({ id: note.id, name: note.title, search_space_id: note.search_space_id });
setShowDeleteNoteDialog(true);
},
},
],
}));
}, [notesData, refetchNotes]);
}, [notesData]);
// Handle add note
const handleAddNote = useCallback(() => {
@ -271,6 +281,7 @@ export function AppSidebarProvider({
navMain={navMain}
RecentChats={[]}
RecentNotes={[]}
onAddNote={handleAddNote}
pageUsage={pageUsage}
/>
);
@ -330,6 +341,49 @@ export function AppSidebarProvider({
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Note Confirmation Dialog */}
<Dialog open={showDeleteNoteDialog} onOpenChange={setShowDeleteNoteDialog}>
<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_note")}</span>
</DialogTitle>
<DialogDescription>
{t("delete_note_confirm")} <span className="font-medium">{noteToDelete?.name}</span>?{" "}
{t("action_cannot_undone")}
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setShowDeleteNoteDialog(false)}
disabled={isDeletingNote}
>
{tCommon("cancel")}
</Button>
<Button
variant="destructive"
onClick={handleDeleteNote}
disabled={isDeletingNote}
className="gap-2"
>
{isDeletingNote ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("deleting")}
</>
) : (
<>
<Trash2 className="h-4 w-4" />
{tCommon("delete")}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -1,6 +1,7 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { Loader2, MessageCircleMore, MoreHorizontal, Search, Trash2, X } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
@ -22,6 +23,7 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { chatsApiService } from "@/lib/apis/chats-api.service";
import { cn } from "@/lib/utils";
@ -94,14 +96,22 @@ export function AllChatsSidebar({
setSearchQuery("");
}, []);
// Filter chats based on search query (client-side filtering)
// Filter and sort chats based on search query (client-side filtering)
const chats = useMemo(() => {
const allChats = chatsData ?? [];
// Sort chats by created_at (most recent first)
const sortedChats = [...allChats].sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return dateB - dateA; // Descending order (most recent first)
});
if (!debouncedSearchQuery) {
return allChats;
return sortedChats;
}
const query = debouncedSearchQuery.toLowerCase();
return allChats.filter((chat) =>
return sortedChats.filter((chat) =>
chat.title.toLowerCase().includes(query)
);
}, [chatsData, debouncedSearchQuery]);
@ -111,7 +121,7 @@ export function AllChatsSidebar({
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col">
<SheetHeader className="px-4 py-4 border-b space-y-3">
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3">
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle>
<SheetDescription className="sr-only">
{t("all_chats_description") || "Browse and manage all your chats"}
@ -135,7 +145,7 @@ export function AllChatsSidebar({
onClick={handleClearSearch}
>
<X className="h-3.5 w-3.5" />
<span className="sr-only">Clear search</span>
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
</Button>
)}
</div>
@ -167,15 +177,22 @@ export function AllChatsSidebar({
)}
>
{/* Main clickable area for navigation */}
<button
type="button"
onClick={() => handleChatClick(chat.id, chat.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{chat.title}</span>
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleChatClick(chat.id, chat.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{chat.title}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
<p>{t("created") || "Created"}: {format(new Date(chat.created_at), "MMM d, yyyy 'at' h:mm a")}</p>
</TooltipContent>
</Tooltip>
{/* Actions dropdown - separate from main click area */}
<DropdownMenu>
@ -195,7 +212,7 @@ export function AllChatsSidebar({
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">More options</span>
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
@ -204,7 +221,7 @@ export function AllChatsSidebar({
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
<span>{t("delete") || "Delete"}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -1,6 +1,7 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
@ -21,6 +22,7 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { notesApiService } from "@/lib/apis/notes-api.service";
@ -121,22 +123,38 @@ export function AllNotesSidebar({
const isLoading = isSearchMode ? isSearching : isLoadingNotes;
const error = isSearchMode ? searchError : notesError;
// Transform notes data - handle both regular notes and search results
// Transform and sort notes data - handle both regular notes and search results
const notes = useMemo(() => {
let notesList: { id: number; title: string; search_space_id: number; created_at: string; updated_at?: string | null }[];
if (isSearchMode && searchData?.items) {
return searchData.items.map((doc) => ({
notesList = searchData.items.map((doc) => ({
id: doc.id,
title: doc.title,
search_space_id: doc.search_space_id,
created_at: doc.created_at,
updated_at: doc.updated_at,
}));
} else {
notesList = notesData?.items ?? [];
}
return notesData?.items ?? [];
// Sort notes by updated_at (most recent first), fallback to created_at
return [...notesList].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)
});
}, [isSearchMode, searchData, notesData]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col">
<SheetHeader className="px-4 py-4 border-b space-y-3">
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3">
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
<SheetDescription className="sr-only">
{t("all_notes_description") || "Browse and manage all your notes"}
@ -160,7 +178,7 @@ export function AllNotesSidebar({
onClick={handleClearSearch}
>
<X className="h-3.5 w-3.5" />
<span className="sr-only">Clear search</span>
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
</Button>
)}
</div>
@ -192,15 +210,27 @@ export function AllNotesSidebar({
)}
>
{/* Main clickable area for navigation */}
<button
type="button"
onClick={() => handleNoteClick(note.id, note.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left"
>
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{note.title}</span>
</button>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleNoteClick(note.id, note.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left"
>
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{note.title}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
<div className="space-y-1">
<p>{t("created") || "Created"}: {format(new Date(note.created_at), "MMM d, yyyy 'at' h:mm a")}</p>
{note.updated_at && (
<p>{t("updated") || "Updated"}: {format(new Date(note.updated_at), "MMM d, yyyy 'at' h:mm a")}</p>
)}
</div>
</TooltipContent>
</Tooltip>
{/* Actions dropdown - separate from main click area */}
<DropdownMenu>
@ -220,7 +250,7 @@ export function AllNotesSidebar({
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">More options</span>
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
@ -229,7 +259,7 @@ export function AllNotesSidebar({
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
<span>{t("delete") || "Delete"}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -273,7 +303,7 @@ export function AllNotesSidebar({
{/* Footer with Add Note button */}
{onAddNote && notes.length > 0 && (
<div className="p-3 border-t">
<div className="mx-3 p-3">
<Button
onClick={() => {
onAddNote();

View file

@ -16,6 +16,7 @@ import {
MessageCircleMore,
MoonIcon,
Podcast,
RefreshCw,
Settings2,
SquareLibrary,
SquareTerminal,
@ -146,6 +147,7 @@ export const iconMap: Record<string, LucideIcon> = {
Trash2,
Podcast,
Users,
RefreshCw,
};
const defaultData = {
@ -293,6 +295,7 @@ export const AppSidebar = memo(function AppSidebar({
const { theme, setTheme } = useTheme();
const { data: user, isPending: isLoadingUser } = useAtomValue(currentUserAtom);
const [isClient, setIsClient] = useState(false);
const [isSourcesExpanded, setIsSourcesExpanded] = useState(false);
useEffect(() => {
setIsClient(true);
@ -444,17 +447,19 @@ export const AppSidebar = memo(function AppSidebar({
</SidebarHeader>
<SidebarContent className="gap-1">
<NavMain items={processedNavMain} />
<NavMain items={processedNavMain} onSourcesExpandedChange={setIsSourcesExpanded} />
<NavChats
chats={processedRecentChats}
searchSpaceId={searchSpaceId}
isSourcesExpanded={isSourcesExpanded}
/>
<NavNotes
notes={processedRecentNotes}
onAddNote={onAddNote}
searchSpaceId={searchSpaceId}
isSourcesExpanded={isSourcesExpanded}
/>
</SidebarContent>
<SidebarFooter>

View file

@ -7,12 +7,12 @@ import {
Loader2,
MessageCircleMore,
MoreHorizontal,
Search,
RefreshCw,
Trash2,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
@ -29,6 +29,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { AllChatsSidebar } from "./all-chats-sidebar";
@ -51,6 +52,7 @@ interface NavChatsProps {
chats: ChatItem[];
defaultOpen?: boolean;
searchSpaceId?: string;
isSourcesExpanded?: boolean;
}
// Map of icon names to their components
@ -58,15 +60,24 @@ const actionIconMap: Record<string, LucideIcon> = {
MessageCircleMore,
Trash2,
MoreHorizontal,
RefreshCw,
};
export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) {
export function NavChats({ chats, defaultOpen = true, searchSpaceId, isSourcesExpanded = false }: NavChatsProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const isMobile = useIsMobile();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
// Auto-collapse on smaller screens when Sources is expanded
useEffect(() => {
if (isSourcesExpanded && isMobile) {
setIsOpen(false);
}
}, [isSourcesExpanded, isMobile]);
// Handle chat deletion with loading state
const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => {
setIsDeleting(chatId);
@ -112,7 +123,7 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
e.stopPropagation();
setIsAllChatsSidebarOpen(true);
}}
aria-label="View all chats"
aria-label={t("view_all_chats") || "View all chats"}
>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
@ -121,10 +132,10 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
</div>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{chats.length > 0 ? (
chats.map((chat) => {
{chats.length > 0 ? (
<SidebarGroupContent>
<SidebarMenu>
{chats.map((chat) => {
const isDeletingChat = isDeleting === chat.id;
return (
@ -163,7 +174,7 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">More options</span>
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right" className="w-40">
@ -191,7 +202,7 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
<ActionIcon className="mr-2 h-4 w-4" />
<span>
{isDeletingChat && isDeleteAction
? "Deleting..."
? t("deleting") || "Deleting..."
: action.name}
</span>
</DropdownMenuItem>
@ -203,17 +214,15 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
)}
</SidebarMenuItem>
);
})
) : (
<SidebarMenuItem>
<SidebarMenuButton disabled className="text-muted-foreground text-xs">
<Search className="h-4 w-4" />
<span>{t("no_recent_chats") || "No recent chats"}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu>
</SidebarGroupContent>
})}
</SidebarMenu>
</SidebarGroupContent>
) : (
<div className="flex items-center gap-2 px-2 py-1 text-muted-foreground/60 text-xs">
<MessageCircleMore className="h-3.5 w-3.5" />
<span>{t("no_recent_chats") || "No recent chats"}</span>
</div>
)}
</CollapsibleContent>
</Collapsible>

View file

@ -2,7 +2,7 @@
import { ChevronRight, type LucideIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
@ -28,7 +28,12 @@ interface NavItem {
}[];
}
export function NavMain({ items }: { items: NavItem[] }) {
interface NavMainProps {
items: NavItem[];
onSourcesExpandedChange?: (expanded: boolean) => void;
}
export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) {
const t = useTranslations("nav_menu");
// Translation function that handles both exact matches and fallback to original
@ -53,6 +58,29 @@ export function NavMain({ items }: { items: NavItem[] }) {
// Memoize items to prevent unnecessary re-renders
const memoizedItems = useMemo(() => items, [items]);
// Track expanded state for items with sub-menus (like Sources)
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>(() => {
const initial: Record<string, boolean> = {};
items.forEach((item) => {
if (item.items?.length) {
initial[item.title] = item.isActive ?? false;
}
});
return initial;
});
// Handle collapsible state change
const handleOpenChange = useCallback(
(title: string, isOpen: boolean) => {
setExpandedItems((prev) => ({ ...prev, [title]: isOpen }));
// Notify parent when Sources is expanded/collapsed
if (title === "Sources" && onSourcesExpandedChange) {
onSourcesExpandedChange(isOpen);
}
},
[onSourcesExpandedChange]
);
return (
<SidebarGroup>
<SidebarGroupLabel>{translateTitle("Platform")}</SidebarGroupLabel>
@ -60,8 +88,15 @@ export function NavMain({ items }: { items: NavItem[] }) {
{memoizedItems.map((item, index) => {
const translatedTitle = translateTitle(item.title);
const hasSub = !!item.items?.length;
const isItemOpen = expandedItems[item.title] ?? item.isActive ?? false;
return (
<Collapsible key={`${item.title}-${index}`} asChild defaultOpen={item.isActive}>
<Collapsible
key={`${item.title}-${index}`}
asChild
open={hasSub ? isItemOpen : undefined}
onOpenChange={hasSub ? (open) => handleOpenChange(item.title, open) : undefined}
defaultOpen={!hasSub ? item.isActive : undefined}
>
<SidebarMenuItem>
{hasSub ? (
// When the item has children, make the whole row a collapsible trigger

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
@ -29,6 +29,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { AllNotesSidebar } from "./all-notes-sidebar";
@ -52,6 +53,7 @@ interface NavNotesProps {
onAddNote?: () => void;
defaultOpen?: boolean;
searchSpaceId?: string;
isSourcesExpanded?: boolean;
}
// Map of icon names to their components
@ -61,13 +63,21 @@ const actionIconMap: Record<string, LucideIcon> = {
MoreHorizontal,
};
export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) {
export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId, isSourcesExpanded = false }: NavNotesProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const isMobile = useIsMobile();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
// Auto-collapse on smaller screens when Sources is expanded
useEffect(() => {
if (isSourcesExpanded && isMobile) {
setIsOpen(false);
}
}, [isSourcesExpanded, isMobile]);
// Handle note deletion with loading state
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
setIsDeleting(noteId);
@ -113,7 +123,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
e.stopPropagation();
setIsAllNotesSidebarOpen(true);
}}
aria-label="View all notes"
aria-label={t("view_all_notes") || "View all notes"}
>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
@ -127,7 +137,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
e.stopPropagation();
onAddNote();
}}
aria-label="Add note"
aria-label={t("add_note") || "Add note"}
>
<Plus className="h-3.5 w-3.5" />
</Button>
@ -178,7 +188,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">More options</span>
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right" className="w-40">
@ -206,7 +216,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
<ActionIcon className="mr-2 h-4 w-4" />
<span>
{isDeletingNote && isDeleteAction
? "Deleting..."
? t("deleting") || "Deleting..."
: action.name}
</span>
</DropdownMenuItem>

View file

@ -352,7 +352,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-hidden group-data-[collapsible=icon]:overflow-hidden",
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}

View file

@ -42,13 +42,13 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance",
"bg-popover text-popover-foreground border shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
<TooltipPrimitive.Arrow className="bg-popover fill-popover z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);

View file

@ -98,6 +98,8 @@
"unknown_search_space": "Unknown Search Space",
"delete_chat": "Delete Chat",
"delete_chat_confirm": "Are you sure you want to delete",
"delete_note": "Delete Note",
"delete_note_confirm": "Are you sure you want to delete",
"action_cannot_undone": "This action cannot be undone.",
"deleting": "Deleting...",
"surfsense_dashboard": "SurfSense Dashboard",
@ -641,7 +643,7 @@
"search_chats": "Search chats...",
"no_chats_found": "No chats found",
"no_recent_chats": "No recent chats",
"view_all_chats": "View All Chats",
"view_all_chats": "View all chats",
"all_chats": "All Chats",
"all_chats_description": "Browse and manage all your chats",
"no_chats": "No chats yet",
@ -659,7 +661,15 @@
"no_notes": "No notes yet",
"create_new_note": "Create a new note",
"error_loading_notes": "Error loading notes",
"loading": "Loading..."
"loading": "Loading...",
"deleting": "Deleting...",
"delete": "Delete",
"created": "Created",
"updated": "Updated",
"more_options": "More options",
"clear_search": "Clear search",
"view_all_notes": "View all notes",
"add_note": "Add note"
},
"errors": {
"something_went_wrong": "Something went wrong",

View file

@ -98,6 +98,8 @@
"unknown_search_space": "未知搜索空间",
"delete_chat": "删除对话",
"delete_chat_confirm": "您确定要删除",
"delete_note": "删除笔记",
"delete_note_confirm": "您确定要删除",
"action_cannot_undone": "此操作无法撤销。",
"deleting": "删除中...",
"surfsense_dashboard": "SurfSense 仪表盘",
@ -659,7 +661,15 @@
"no_notes": "暂无笔记",
"create_new_note": "创建新笔记",
"error_loading_notes": "加载笔记时出错",
"loading": "加载中..."
"loading": "加载中...",
"deleting": "删除中...",
"delete": "删除",
"created": "创建时间",
"updated": "更新时间",
"more_options": "更多选项",
"clear_search": "清除搜索",
"view_all_notes": "查看所有笔记",
"add_note": "添加笔记"
},
"errors": {
"something_went_wrong": "出错了",