'use client' import Link from 'next/link' import { usePathname, useSearchParams, useRouter } from 'next/navigation' import { cn } from '@/lib/utils' import { Settings, Plus, ChevronRight, Lock, BookOpen, Bot, Inbox, FlaskConical, ArrowUpDown, Archive, MessageSquare, Sparkles, Trash2, User, LogOut, Shield, GripVertical, Users, Bell, } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { useEffect, useMemo, useRef, useState } from 'react' import { getAllNotes } from '@/app/actions/notes' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { useNotebooks } from '@/context/notebooks-context' import { Notebook, Note } from '@/lib/types' import { motion, AnimatePresence } from 'motion/react' import { getNoteDisplayTitle } from '@/lib/note-preview' import { CreateNotebookDialog } from './create-notebook-dialog' import { NotificationPanel } from './notification-panel' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { signOut } from 'next-auth/react' import { useNoteRefresh } from '@/context/NoteRefreshContext' type NavigationView = 'notebooks' | 'agents' type SortOrder = 'newest' | 'oldest' | 'alpha' function NoteLink({ title, isActive, onClick, }: { title: string isActive: boolean onClick: () => void }) { return (
{title} ) } function SidebarCarnetItem({ carnet, isActive, notes, activeNoteId, onCarnetClick, onNoteClick, isDragging, dragHandleProps, }: { carnet: { id: string; name: string; initial: string; isPrivate?: boolean } isActive: boolean notes: { id: string; title: string }[] activeNoteId: string | null onCarnetClick: () => void onNoteClick: (noteId: string, carnetId: string) => void isDragging?: boolean dragHandleProps?: React.HTMLAttributes }) { const { t } = useLanguage() return (
{/* Drag handle — visible on hover */}
{carnet.initial}
{carnet.name} {carnet.isPrivate && }
{isActive && ( {notes.map(note => ( onNoteClick(note.id, carnet.id)} /> ))} {notes.length === 0 && (

{t('common.noResults')}

)}
)}
) } export function Sidebar({ className, user }: { className?: string; user?: any }) { const pathname = usePathname() const searchParams = useSearchParams() const router = useRouter() const { t } = useLanguage() const { notebooks, updateNotebookOrderOptimistic } = useNotebooks() const { refreshKey } = useNoteRefresh() const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [notebookNotes, setNotebookNotes] = useState>({}) const [activeView, setActiveView] = useState('notebooks') const [sortOrder, setSortOrder] = useState('newest') const [showSortMenu, setShowSortMenu] = useState(false) // ── Drag state ── const [draggedId, setDraggedId] = useState(null) const [orderedNotebooks, setOrderedNotebooks] = useState([]) const dragOverId = useRef(null) // Prevents the sync effect from overwriting a just-saved drag order const isSavingRef = useRef(false) const currentNotebookId = searchParams.get('notebook') const currentNoteId = searchParams.get('openNote') const isInboxActive = pathname === '/' && !searchParams.get('notebook') && !searchParams.get('labels') && !searchParams.get('archived') && !searchParams.get('trashed') useEffect(() => { setActiveView( pathname.startsWith('/agents') || pathname.startsWith('/lab') ? 'agents' : 'notebooks' ) }, [pathname]) const displayName = user?.name || user?.email || '' const initial = displayName ? displayName.charAt(0).toUpperCase() : '?' // Sorted list for the sort dropdown (not used directly when dragging) const sortedNotebooks = useMemo(() => { const arr = [...notebooks] if (sortOrder === 'alpha') return arr.sort((a, b) => a.name.localeCompare(b.name)) if (sortOrder === 'newest') return arr.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) if (sortOrder === 'oldest') return arr.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) return arr }, [notebooks, sortOrder]) // Sync orderedNotebooks from server ONLY when not in the middle of a drag save useEffect(() => { if (isSavingRef.current) return setOrderedNotebooks(sortedNotebooks) }, [sortedNotebooks]) const notebookIdsKey = useMemo(() => notebooks.map(nb => nb.id).sort().join(','), [notebooks]) useEffect(() => { if (!notebookIdsKey) return let cancelled = false const load = async () => { const mappedEntries = await Promise.all( notebooks.map(async (nb: Notebook) => { const notes = await getAllNotes(false, nb.id) const mapped = notes.map((n: Note) => ({ id: n.id, title: getNoteDisplayTitle(n, t('notes.untitled')), })) return [nb.id, mapped] as const }) ) if (cancelled) return setNotebookNotes(Object.fromEntries(mappedEntries)) } load() return () => { cancelled = true } // refreshKey: reload note titles whenever any note is saved/created/deleted }, [notebookIdsKey, refreshKey, t]) const handleCarnetClick = (notebookId: string) => { const params = new URLSearchParams() params.set('notebook', notebookId) params.set('forceList', '1') router.push(`/?${params.toString()}`) } const handleInboxClick = () => { router.push('/?forceList=1') } const handleNoteClick = (noteId: string, notebookId: string) => { const params = new URLSearchParams(searchParams.toString()) params.set('notebook', notebookId) params.set('openNote', noteId) params.delete('forceList') router.push(`/?${params.toString()}`) } // ── Drag handlers ── const handleDragStart = (e: React.DragEvent, notebookId: string) => { setDraggedId(notebookId) e.dataTransfer.effectAllowed = 'move' } const handleDragOver = (e: React.DragEvent, notebookId: string) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' if (dragOverId.current === notebookId) return dragOverId.current = notebookId if (!draggedId || draggedId === notebookId) return setOrderedNotebooks(prev => { const fromIdx = prev.findIndex(n => n.id === draggedId) const toIdx = prev.findIndex(n => n.id === notebookId) if (fromIdx === -1 || toIdx === -1) return prev const next = [...prev] const [item] = next.splice(fromIdx, 1) next.splice(toIdx, 0, item) return next }) } const handleDrop = async (e: React.DragEvent) => { e.preventDefault() if (!draggedId) return const savedOrder = [...orderedNotebooks] setDraggedId(null) dragOverId.current = null // Block the sync effect so the server reload doesn't overwrite local order isSavingRef.current = true try { await updateNotebookOrderOptimistic(savedOrder.map(n => n.id)) // Keep local order — server will return them in the right order next load setOrderedNotebooks(savedOrder) } catch { // On failure, revert to original server order isSavingRef.current = false setOrderedNotebooks(sortedNotebooks) } finally { // Allow sync again after 2 s (time for the server reload to settle) setTimeout(() => { isSavingRef.current = false }, 2000) } } const handleDragEnd = () => { if (draggedId) { // Drag cancelled without drop — restore setDraggedId(null) dragOverId.current = null isSavingRef.current = false setOrderedNotebooks(sortedNotebooks) } } const sortLabels: Record = { newest: t('sidebar.sortNewest'), oldest: t('sidebar.sortOldest'), alpha: t('sidebar.sortAlpha'), } return ( <> ) }