mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
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:
parent
60d57305a7
commit
bb971f89ba
11 changed files with 293 additions and 113 deletions
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "出错了",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue