- Add AI Provider Testing page (/admin/ai-test) with Tags and Embeddings tests - Add new AI providers: CustomOpenAI, DeepSeek, OpenRouter - Add API routes for AI config, models listing, and testing endpoints - Add UX Design Specification document for Phase 1 MVP AI - Add PRD Phase 1 MVP AI planning document - Update admin settings and sidebar navigation - Fix AI factory for multi-provider support
202 lines
7.4 KiB
TypeScript
202 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { usePathname, useSearchParams } from 'next/navigation'
|
|
import { cn } from '@/lib/utils'
|
|
import { StickyNote, Bell, Archive, Trash2, Tag, ChevronDown, ChevronUp, Settings, User, Shield, Coffee, LogOut } from 'lucide-react'
|
|
import { useLabels } from '@/context/LabelContext'
|
|
import { LabelManagementDialog } from './label-management-dialog'
|
|
import { useSession, signOut } from 'next-auth/react'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { useRouter } from 'next/navigation'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
import { LABEL_COLORS } from '@/lib/types'
|
|
|
|
export function Sidebar({ className, user }: { className?: string, user?: any }) {
|
|
const pathname = usePathname()
|
|
const searchParams = useSearchParams()
|
|
const router = useRouter()
|
|
const { labels, getLabelColor } = useLabels()
|
|
const [isLabelsExpanded, setIsLabelsExpanded] = useState(false)
|
|
const { data: session } = useSession()
|
|
|
|
const currentUser = user || session?.user
|
|
|
|
const currentLabels = searchParams.get('labels')?.split(',').filter(Boolean) || []
|
|
const currentSearch = searchParams.get('search')
|
|
|
|
// Show first 5 labels by default, or all if expanded
|
|
const displayedLabels = isLabelsExpanded ? labels : labels.slice(0, 5)
|
|
const hasMoreLabels = labels.length > 5
|
|
|
|
const userRole = (currentUser as any)?.role || 'USER'
|
|
const userInitials = currentUser?.name
|
|
? currentUser.name.split(' ').map((n: string) => n[0]).join('').toUpperCase().substring(0, 2)
|
|
: 'U'
|
|
|
|
const NavItem = ({ href, icon: Icon, label, active, onClick, iconColorClass }: any) => (
|
|
<Link
|
|
href={href}
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors",
|
|
active
|
|
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
|
|
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
|
|
)}
|
|
>
|
|
<Icon className={cn("h-5 w-5", active && "fill-current", !active && iconColorClass)} />
|
|
<span className="truncate">{label}</span>
|
|
</Link>
|
|
)
|
|
|
|
return (
|
|
<aside className={cn("w-[280px] flex-col gap-1 overflow-y-auto overflow-x-hidden hidden md:flex", className)}>
|
|
{/* User Profile Section - Top of Sidebar */}
|
|
{currentUser && (
|
|
<div className="p-4 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/50">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button className="flex items-center gap-3 w-full p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors text-left">
|
|
<Avatar className="h-10 w-10 ring-2 ring-amber-500/20">
|
|
<AvatarImage src={currentUser.image || ''} alt={currentUser.name || ''} />
|
|
<AvatarFallback className="bg-amber-500 text-white font-medium">
|
|
{userInitials}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
{currentUser.name}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
|
{currentUser.email}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-56" forceMount>
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium leading-none">{currentUser.name}</p>
|
|
<p className="text-xs leading-none text-muted-foreground">
|
|
{currentUser.email}
|
|
</p>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem onClick={() => router.push('/settings/profile')}>
|
|
<User className="mr-2 h-4 w-4" />
|
|
<span>Profile</span>
|
|
</DropdownMenuItem>
|
|
{userRole === 'ADMIN' && (
|
|
<DropdownMenuItem onClick={() => router.push('/admin')}>
|
|
<Shield className="mr-2 h-4 w-4" />
|
|
<span>Admin Dashboard</span>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem onClick={() => router.push('/settings')}>
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
<span>Diagnostics</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => signOut({ callbackUrl: '/login' })}>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
<span>Log out</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)}
|
|
|
|
{/* Navigation Items */}
|
|
<div className="py-2">
|
|
<NavItem
|
|
href="/"
|
|
icon={StickyNote}
|
|
label="Notes"
|
|
active={pathname === '/' && currentLabels.length === 0 && !currentSearch}
|
|
/>
|
|
<NavItem
|
|
href="/reminders"
|
|
icon={Bell}
|
|
label="Reminders"
|
|
active={pathname === '/reminders'}
|
|
/>
|
|
|
|
<div className="my-2 px-4 flex items-center justify-between group">
|
|
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Labels</span>
|
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<LabelManagementDialog />
|
|
</div>
|
|
</div>
|
|
|
|
{displayedLabels.map(label => {
|
|
const colorName = getLabelColor(label.name)
|
|
const colorClass = LABEL_COLORS[colorName]?.icon
|
|
|
|
return (
|
|
<NavItem
|
|
key={label.id}
|
|
href={`/?labels=${encodeURIComponent(label.name)}`}
|
|
icon={Tag}
|
|
label={label.name}
|
|
active={currentLabels.includes(label.name)}
|
|
iconColorClass={colorClass}
|
|
/>
|
|
)
|
|
})}
|
|
|
|
{hasMoreLabels && (
|
|
<button
|
|
onClick={() => setIsLabelsExpanded(!isLabelsExpanded)}
|
|
className="flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors w-full hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
|
|
>
|
|
{isLabelsExpanded ? (
|
|
<ChevronUp className="h-5 w-5" />
|
|
) : (
|
|
<ChevronDown className="h-5 w-5" />
|
|
)}
|
|
<span>{isLabelsExpanded ? 'Show less' : 'Show more'}</span>
|
|
</button>
|
|
)}
|
|
|
|
<div className="my-2 border-t border-gray-200 dark:border-zinc-800" />
|
|
|
|
<NavItem
|
|
href="/archive"
|
|
icon={Archive}
|
|
label="Archive"
|
|
active={pathname === '/archive'}
|
|
/>
|
|
<NavItem
|
|
href="/trash"
|
|
icon={Trash2}
|
|
label="Trash"
|
|
active={pathname === '/trash'}
|
|
/>
|
|
|
|
<NavItem
|
|
href="/support"
|
|
icon={Coffee}
|
|
label="Support Memento ☕"
|
|
active={pathname === '/support'}
|
|
/>
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|
|
|