From 626524b1868ca2ba0ca8a265ad019ceb80cebba9 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 17 Jul 2026 17:55:55 +0800 Subject: [PATCH] feat: compose Jyotisha app sidebar --- frontend/src/components/app-sidebar.tsx | 221 +++++++++++++++++++++++ frontend/tests/sidebar-contract.test.ts | 43 +++++ frontend/tests/starter-questions.test.ts | 6 +- 3 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/app-sidebar.tsx diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx new file mode 100644 index 00000000..73ec31cf --- /dev/null +++ b/frontend/src/components/app-sidebar.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { Popover } from "@base-ui/react/popover"; +import Link from "next/link"; +import { + ChevronRight, + Gift, + KeyRound, + LogOut, + MessageSquareText, + Plus, + UserRound, +} from "lucide-react"; +import { useEffect, useRef } from "react"; +import type { Ref } from "react"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from "@/components/ui/sidebar"; + +export type SidebarSession = { + id: string; + title: string; + messageCount: number; +}; + +export type SidebarAccount = { + name: string; + email: string; + credits: number; + isAdmin: boolean; + initial: string; +}; + +export type AppSidebarProps = { + sessions: readonly SidebarSession[]; + activeSessionId: string | null; + account: SidebarAccount; + accountMenuOpen: boolean; + accountTriggerRef: Ref; + newChatDisabled: boolean; + creatingSession: boolean; + onAccountMenuOpenChange: (open: boolean) => void; + onNewChat: () => void; + onSelectSession: (sessionId: string) => void; + onOpenProfile: () => void; + onOpenRedeem: () => void; + onOpenLogout: () => void; +}; + +export function AppSidebar({ + sessions, + activeSessionId, + account, + accountMenuOpen, + accountTriggerRef, + newChatDisabled, + creatingSession, + onAccountMenuOpenChange, + onNewChat, + onSelectSession, + onOpenProfile, + onOpenRedeem, + onOpenLogout, +}: AppSidebarProps) { + const { isMobile, setOpen, setOpenMobile, state } = useSidebar(); + const firstSessionRef = useRef(null); + const historyHeadingRef = useRef(null); + const isCollapsedDesktop = state === "collapsed" && !isMobile; + const showExpandedContent = !isCollapsedDesktop; + const popoverPlacement = `${isMobile}:${state}`; + const previousPopoverPlacement = useRef(popoverPlacement); + + useEffect(() => { + if (previousPopoverPlacement.current !== popoverPlacement && accountMenuOpen) { + onAccountMenuOpenChange(false); + } + previousPopoverPlacement.current = popoverPlacement; + }, [accountMenuOpen, onAccountMenuOpenChange, popoverPlacement]); + + function handleNewChat() { + onNewChat(); + if (isMobile) setOpenMobile(false); + } + + function handleExpandHistory() { + setOpen(true); + window.requestAnimationFrame(() => { + (firstSessionRef.current ?? historyHeadingRef.current)?.focus(); + }); + } + + function handleAccountAction(action: () => void) { + onAccountMenuOpenChange(false); + action(); + } + + return ( + + +
+
+ + + + + + +
+ + + {showExpandedContent ? ( + + 聊天记录 + + {sessions.length === 0 ?

暂无对话

: ( + + {sessions.map((session, index) => ( + + { + onSelectSession(session.id); + if (isMobile) setOpenMobile(false); + }} + > + {session.title} + {session.messageCount > 0 ? {session.messageCount} 条消息 : null} + + + ))} + + )} +
+
+ ) : ( + + + + + + + )} +
+ + + + + + {showExpandedContent ? {account.name} : null} + {showExpandedContent ? + + + +
+ + {account.name}{account.email} +
+ + + {account.isAdmin && onAccountMenuOpenChange(false)}> +