'use client'; import { cn } from '@/lib/utils'; import { MessagesSquare, Plus, Newspaper, TrendingUp, Plane, LayoutPanelLeft, User, Stethoscope, Building2, Package, SlidersHorizontal, Baby, GraduationCap, HeartPulse, Brain, Trophy, ShoppingCart, Gamepad2, Receipt, Scale, MoreHorizontal, } from 'lucide-react'; import Link from 'next/link'; import { useSelectedLayoutSegments } from 'next/navigation'; import React, { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from '@/lib/localization/context'; import { getSidebarMenuConfig } from '@/lib/config/sidebarMenu'; import MenuSettingsPanel from './Sidebar/MenuSettingsPanel'; import Layout from './Layout'; interface ChatItem { id: string; title: string; createdAt: string; } const VerticalIconContainer = ({ children }: { children: ReactNode }) => { return
{children}
; }; interface HistorySubmenuProps { onMouseEnter: () => void; onMouseLeave: () => void; } interface MoreSubmenuProps { onMouseEnter: () => void; onMouseLeave: () => void; restIds: string[]; linkMap: Record; itemLabels: Record; submenuStyle: { top: number; left: number }; isMobile: boolean; cancelMoreHide: () => void; } const MoreSubmenu = ({ onMouseEnter, onMouseLeave, restIds, linkMap, itemLabels, submenuStyle, isMobile, cancelMoreHide, }: MoreSubmenuProps) => { const { t } = useTranslation(); const [settingsHovered, setSettingsHovered] = useState(false); return (
); }; const HistorySubmenu = ({ onMouseEnter, onMouseLeave }: HistorySubmenuProps) => { const { t } = useTranslation(); const [chats, setChats] = useState([]); const [loading, setLoading] = useState(true); const fetchChats = useCallback(async () => { try { const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') ?? localStorage.getItem('access_token') : null; if (!token) { const { getGuestChats } = await import('@/lib/guest-storage'); const guestChats = getGuestChats(); setChats( guestChats.map((c) => ({ id: c.id, title: c.title, createdAt: c.createdAt, sources: c.sources, files: c.files, })), ); setLoading(false); return; } const res = await fetch('/api/v1/library/threads', { headers: { Authorization: `Bearer ${token}` }, }); const data = await res.json(); setChats((data.chats ?? []).reverse()); } catch { setChats([]); } finally { setLoading(false); } }, []); useEffect(() => { fetchChats(); }, [fetchChats]); useEffect(() => { const onMigrated = () => fetchChats(); window.addEventListener('gooseek:guest-migrated', onMigrated); return () => window.removeEventListener('gooseek:guest-migrated', onMigrated); }, []); return (
{loading ? (
{t('common.loading')}
) : chats.length === 0 ? (
) : ( )}
); }; type NavLink = { icon: React.ComponentType<{ size?: number | string; className?: string }>; href?: string; active: boolean; label: string; }; const Sidebar = ({ children }: { children: React.ReactNode }) => { const segments = useSelectedLayoutSegments(); const { t } = useTranslation(); const [historyOpen, setHistoryOpen] = useState(false); const [moreOpen, setMoreOpen] = useState(false); const historyRef = useRef(null); const moreRef = useRef(null); const hideTimeoutRef = useRef | null>(null); const moreHideRef = useRef | null>(null); const [submenuStyle, setSubmenuStyle] = useState({ top: 0, left: 0 }); const [tooltip, setTooltip] = useState<{ label: string; x: number; y: number } | null>(null); const [menuConfig, setMenuConfig] = useState(() => typeof window !== 'undefined' ? getSidebarMenuConfig() : { order: [], visible: {} }, ); const [isMobile, setIsMobile] = useState(false); useEffect(() => { if (typeof window === 'undefined') return; const mq = window.matchMedia('(max-width: 1023px)'); setIsMobile(mq.matches); const handler = () => setIsMobile(mq.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); const discoverLink = { icon: Newspaper, href: '/discover', active: segments.includes('discover'), label: t('nav.discover'), }; const libraryLink = { icon: MessagesSquare, href: '/library', active: segments.includes('library'), label: t('nav.messageHistory'), }; const financeLink = { icon: TrendingUp, href: '/finance', active: segments.includes('finance'), label: t('nav.finance'), }; const travelLink = { icon: Plane, href: '/travel', active: segments.includes('travel'), label: t('nav.travel'), }; const spacesLink = { icon: LayoutPanelLeft, href: '/spaces', active: segments.includes('spaces'), label: t('nav.spaces'), }; const profileLink = { icon: User, href: '/profile', active: segments.includes('profile'), label: t('nav.profile'), }; const placeholderLink = ( icon: React.ComponentType<{ size?: number | string; className?: string }>, labelKey: string, ) => ({ icon, href: undefined as string | undefined, active: false, label: t(labelKey), }); const medicineLink = placeholderLink(Stethoscope, 'nav.medicine'); const realEstateLink = placeholderLink(Building2, 'nav.realEstate'); const goodsLink = placeholderLink(Package, 'nav.goods'); const childrenLink = placeholderLink(Baby, 'nav.children'); const educationLink = placeholderLink(GraduationCap, 'nav.education'); const healthLink = placeholderLink(HeartPulse, 'nav.health'); const psychologyLink = placeholderLink(Brain, 'nav.psychology'); const sportsLink = placeholderLink(Trophy, 'nav.sports'); const shoppingLink = placeholderLink(ShoppingCart, 'nav.shopping'); const gamesLink = placeholderLink(Gamepad2, 'nav.games'); const taxesLink = placeholderLink(Receipt, 'nav.taxes'); const legislationLink = placeholderLink(Scale, 'nav.legislation'); const placeholderLinks = [ medicineLink, realEstateLink, goodsLink, childrenLink, educationLink, healthLink, psychologyLink, sportsLink, shoppingLink, gamesLink, taxesLink, legislationLink, ]; const linkMap: Record = { discover: discoverLink, library: libraryLink, finance: financeLink, travel: travelLink, spaces: spacesLink, medicine: medicineLink, realEstate: realEstateLink, goods: goodsLink, children: childrenLink, education: educationLink, health: healthLink, psychology: psychologyLink, sports: sportsLink, shopping: shoppingLink, games: gamesLink, taxes: taxesLink, legislation: legislationLink, profile: profileLink, }; const itemLabels: Record = Object.fromEntries( Object.entries(linkMap).map(([id, link]) => [id, link.label]), ); const orderedVisibleIds = menuConfig.order.filter((id) => menuConfig.visible[id] !== false); const mainIds = orderedVisibleIds.filter((id) => id !== 'profile' && id !== 'spaces'); const topMainIds = mainIds.slice(0, 5); const restMainIds = mainIds.slice(5); const showSpaces = menuConfig.visible['spaces'] !== false; const showProfile = menuConfig.visible['profile'] !== false; const refreshMenuConfig = useCallback(() => { setMenuConfig(getSidebarMenuConfig()); }, []); useEffect(() => { const handler = () => refreshMenuConfig(); window.addEventListener('client-config-changed', handler); return () => window.removeEventListener('client-config-changed', handler); }, [refreshMenuConfig]); const handleHistoryMouseEnter = () => { setTooltip(null); if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } if (historyRef.current) { const rect = historyRef.current.getBoundingClientRect(); setSubmenuStyle({ top: 0, left: rect.right + 4 }); } setHistoryOpen(true); }; const handleHistoryMouseLeave = () => { hideTimeoutRef.current = setTimeout(() => setHistoryOpen(false), 150); }; const cancelHide = () => { if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } }; const handleMoreEnter = () => { setTooltip(null); if (moreHideRef.current) { clearTimeout(moreHideRef.current); moreHideRef.current = null; } if (moreRef.current) { const rect = moreRef.current.getBoundingClientRect(); setSubmenuStyle({ top: 0, left: rect.right + 4 }); } setMoreOpen(true); }; const handleMoreLeave = () => { moreHideRef.current = setTimeout(() => setMoreOpen(false), 150); }; const cancelMoreHide = () => { if (moreHideRef.current) { clearTimeout(moreHideRef.current); moreHideRef.current = null; } }; useEffect(() => { return () => { if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); if (moreHideRef.current) clearTimeout(moreHideRef.current); }; }, []); useEffect(() => { if (!moreOpen || !isMobile) return; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setMoreOpen(false); }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [moreOpen, isMobile]); const showTooltip = (label: string, e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); setTooltip({ label, x: rect.right + 8, y: rect.top + rect.height / 2 }); }; const hideTooltip = () => setTooltip(null); return (
{tooltip && typeof document !== 'undefined' && createPortal(
{tooltip.label}
, document.body, )}
showTooltip(t('chat.newChat'), e)} onMouseLeave={hideTooltip} > {showSpaces && ( showTooltip(spacesLink.label, e)} onMouseLeave={hideTooltip} >
)} {topMainIds.map((id) => { if (id === 'library') { return (
showTooltip(libraryLink.label, e)} onMouseLeave={hideTooltip} >
{historyOpen && typeof document !== 'undefined' && createPortal(
, document.body, )}
); } const link = linkMap[id]; if (!link) return null; if (link.href) { return ( showTooltip(link.label, e)} onMouseLeave={hideTooltip} >
); } return ( showTooltip(link.label, e)} onMouseLeave={hideTooltip} >
); })}
{moreOpen && typeof document !== 'undefined' && createPortal( isMobile ? (
setMoreOpen(false)} role="button" tabIndex={0} aria-label={t('common.close')} >
e.stopPropagation()} onMouseEnter={cancelMoreHide} onMouseLeave={() => {}} > setMoreOpen(false)} cancelMoreHide={cancelMoreHide} />
) : (
), document.body, )}
{showProfile && ( showTooltip(profileLink.label, e)} onMouseLeave={hideTooltip} >
)}
{topMainIds.map((id) => { const link = linkMap[id]; if (!link) return null; return link.href ? ( {link.active && (
)}

{link.label}

) : (

{link.label}

); })} {showProfile && ( {profileLink.active && (
)}

{profileLink.label}

)}
{children}
); }; export default Sidebar;