diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index fa6e0911..79198268 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,14 +1,16 @@ "use client"; import Link from "next/link"; -import { ArrowUp, ArrowUpRight, ChevronRight, Gift, KeyRound, LogOut, Menu, Plus, Sparkles, Square, UserRound, X } from "lucide-react"; +import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { FormEvent, KeyboardEvent } from "react"; +import { AppSidebar } from "@/components/app-sidebar"; import { BirthTimeIntakeFields } from "@/components/birth-time-intake"; import { BirthTimeRectification } from "@/components/birth-time-rectification"; import { ChatMessageContent } from "@/components/chat-message-content"; import { ModelSelector } from "@/components/model-selector"; import { Button } from "@/components/ui/button"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { Textarea } from "@/components/ui/textarea"; import { chinaLocations, type ProvinceNode } from "@/data/china-locations"; import { parseAgentReply, type ReplyTheme } from "@/lib/agent-reply"; @@ -516,7 +518,6 @@ export default function Home() { const [profileDraft, setProfileDraft] = useState(emptyProfile); const [accountMenuOpen, setAccountMenuOpen] = useState(false); const [activeAccountDialog, setActiveAccountDialog] = useState(null); - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [profileNotice, setProfileNotice] = useState(""); const [account, setAccount] = useState(null); const [accountError, setAccountError] = useState(""); @@ -551,13 +552,9 @@ export default function Home() { const [presetMessageLength, setPresetMessageLength] = useState(0); const conversationEnd = useRef(null); const accountTrigger = useRef(null); - const accountMenu = useRef(null); const accountDialog = useRef(null); const creditTrigger = useRef(null); const dialogReturnTarget = useRef(null); - const mobileMenuTrigger = useRef(null); - const sidebar = useRef(null); - const sidebarCloseButton = useRef(null); const closeButton = useRef(null); const redeemInput = useRef(null); const composerInput = useRef(null); @@ -855,42 +852,6 @@ export default function Home() { } }, [accountId, activeAccountDialog, hydrated, onboardingStep, presetMessageFinished, profileComplete]); - useEffect(() => { - if (!mobileSidebarOpen) return; - window.requestAnimationFrame(() => sidebarCloseButton.current?.focus()); - const closeOnEscape = (event: globalThis.KeyboardEvent) => { - if (event.key === "Escape") { - if (accountMenuOpen) return; - setMobileSidebarOpen(false); - window.requestAnimationFrame(() => mobileMenuTrigger.current?.focus()); - return; - } - const container = sidebar.current; - if (container) keepFocusWithin(event, container); - }; - window.addEventListener("keydown", closeOnEscape); - return () => window.removeEventListener("keydown", closeOnEscape); - }, [accountMenuOpen, mobileSidebarOpen]); - - useEffect(() => { - if (!accountMenuOpen) return; - const dismissMenu = (event: MouseEvent) => { - const target = event.target; - if (target instanceof Node && !accountMenu.current?.contains(target)) setAccountMenuOpen(false); - }; - const closeOnEscape = (event: globalThis.KeyboardEvent) => { - if (event.key !== "Escape") return; - setAccountMenuOpen(false); - window.requestAnimationFrame(() => accountTrigger.current?.focus()); - }; - document.addEventListener("mousedown", dismissMenu); - window.addEventListener("keydown", closeOnEscape); - return () => { - document.removeEventListener("mousedown", dismissMenu); - window.removeEventListener("keydown", closeOnEscape); - }; - }, [accountMenuOpen]); - useEffect(() => { if (activeAccountDialog === null) return; window.requestAnimationFrame(() => { @@ -957,7 +918,6 @@ export default function Home() { async function startNewChat() { if (!account || !modelCatalog || creatingSession) return; - setMobileSidebarOpen(false); const nextSession = createSession(modelCatalog.defaultModelId); const previousSessionId = activeSession?.id ?? ""; setCreatingSession(true); @@ -981,6 +941,12 @@ export default function Home() { } } + function selectSession(sessionId: string) { + setActiveSessionId(sessionId); + setDraft(""); + setComposerNotice(""); + } + async function selectSessionModel(modelId: string) { const userId = account?.user.id; if (!activeSession || !modelCatalog || !userId || pendingSessionId || cancellationPending || creatingSession) return; @@ -1033,12 +999,6 @@ export default function Home() { } } - function toggleAccountMenu() { - setActiveAccountDialog(null); - setAccountError(""); - setAccountMenuOpen((current) => !current); - } - function openAccountDialog(dialog: AccountDialog, returnTarget: HTMLButtonElement | null = accountTrigger.current) { dialogReturnTarget.current = returnTarget ?? accountTrigger.current; setAccountMenuOpen(false); @@ -1719,59 +1679,43 @@ export default function Home() { ); } - return ( -
- - - -
- - {accountMenuOpen && ( - - + const sidebarAccount = { + name: profile.name.trim() || account.user.email || "账户", + email: account.user.email || "尚未读取邮箱", + credits: account.credits, + isAdmin: account.isAdmin, + initial: profile.name.trim().slice(0, 1) + || account.user.email?.slice(0, 1).toUpperCase() + || "你", + }; -
-
- + const sidebarSessions = sessions.map((session) => ({ + id: session.id, + title: session.title, + messageCount: session.messages.length, + })); + + return ( + +
+ void startNewChat()} + onSelectSession={selectSession} + onOpenProfile={() => openAccountDialog("profile")} + onOpenRedeem={() => openAccountDialog("redeem")} + onOpenLogout={() => openAccountDialog("logout")} + /> + +
+
{activeSession?.title || "新对话"} {isLoading @@ -1784,7 +1728,7 @@ export default function Home() {
+
{!activeSession?.messages.length ? ( @@ -1968,7 +1912,7 @@ export default function Home() { : "Enter 发送 · Shift + Enter 换行")}

- + {activeAccountDialog !== null && (
@@ -2017,6 +1961,7 @@ export default function Home() {
)} -
+ + ); } diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 13941540..facceb1c 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -25,6 +25,7 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarRail, + SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; @@ -111,6 +112,7 @@ export function AppSidebar({
diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index 6f0beb42..3ae7d827 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -95,6 +95,12 @@ test("composes the Jyotisha app sidebar from the generic shell", () => { } }); +test("renders the mobile drawer close trigger in the app sidebar brand row", () => { + const appSidebar = readProjectFile("src/components/app-sidebar.tsx"); + + assert.match(appSidebar, /
[\s\S]*[\s\S]*<\/div>/); +}); + test("uses one collapsed history action instead of icon-only session rows", () => { const appSidebar = readProjectFile("src/components/app-sidebar.tsx"); assert.match(appSidebar, /MessageSquareText/); @@ -132,3 +138,40 @@ test("keeps app sidebar props as product data and callbacks", () => { assert.match(appSidebar, /onSelectSession: \(sessionId: string\) => void/); assert.doesNotMatch(appSidebar, /supabase|fetch\(|\/api\//i); }); + +test("composes the chat page with the app sidebar shell", () => { + const page = readProjectFile("src/app/page.tsx"); + + assert.match(page, //); + assert.match(page, /
[\s\S]*/); + assert.match(page, //); +}); + +test("removes page-local mobile sidebar ownership", () => { + const page = readProjectFile("src/app/page.tsx"); + + assert.doesNotMatch(page, /mobileSidebarOpen|setMobileSidebarOpen/); + assert.doesNotMatch(page, /className="sidebar-backdrop"/); + assert.doesNotMatch(page, /