diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 6ab5e490..2b283dee 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -508,14 +508,29 @@ one skeleton, none of them carrying the sidebar. `.session-main`, so they lost the 44px minimum, the row padding and the 2px current-item marker, while `.session-row` still reserved a 44px column for a menu button it never rendered. -- **What read-only drops — exactly three things.** The per-row menu trigger (and - with it the reserved column, via `.session-row[data-readonly="true"]`), the - footer chevron, and the account menu behind it. Everything else is the same - element, the same class and the same size: brand row, 新建对话, the three page - actions, the flat 「最近」 list, the 56px `.profile-trigger`. Rows are links to - `/?c=`; the footer is a link to `/`. Renaming, pinning, archiving and - deleting stay on `/`, backed by `Home()`'s optimistic-update and rollback - layer, which is far more than these four routes need. +- **What read-only drops — exactly one thing.** The per-row menu trigger (and + with it the reserved column, via `.session-row[data-readonly="true"]`). + Everything else is the same element, the same class and the same size: brand + row, 新建对话, the page actions, the flat 「最近」 list, the 56px + `.profile-trigger` with its chevron. Rows are links to `/?c=`. + Renaming, pinning, archiving and deleting stay on `/`, backed by `Home()`'s + optimistic-update and rollback layer, which is far more than these routes + need. +- **The avatar opens the account menu on every page (2026-09-28, + TASK-self-edit-avatar-menu-20260928 S2).** It used to be a plain link to `/` + off the home page, so clicking the avatar on 星盘 / 星历 / 我的报告 / 星盘档案 + just left the page. Now it is the same `Menu.Root` everywhere — same popup, + same identity block, same items. On `/` Home controls it and 个人资料 / 通用设置 + / 账户与点数 / 退出登录 open their dialogs in place. Elsewhere the menu keeps + its own open state and those four entries are links to `/?account=`: + Home reads the intent once on mount, drops it from the address bar at once + (a reload or Back never reopens it), and opens that dialog when its account + is ready — no waiting state of its own. The theme choice is not a dialog and + applies where the reader is. While the account is still being read the + footer is the same 56px block, inert and without a link; signed out it is + 去登录. The dialogs themselves stay on `/`; moving them into every page would + add a loading state to the home first frame (§ reveal rule), so it was not + done. - **New chat is not a home link.** The read-only 新建对话 action links to `/?new=1`; Home consumes that intent before activation and opens a fresh local consultation. It clears `new`, any `c`, and the login-return stub. Before the @@ -523,8 +538,7 @@ one skeleton, none of them carrying the sidebar. The first question uses the existing persistence flow. A reserved older consultation keeps recovering in the background without taking over this explicit new-chat landing or leaving its startup recovery notice there. - The account footer - still links to `/`, and a bare-home refresh keeps its existing landing rule. + A bare-home refresh keeps its existing landing rule. The same action and mobile drawer-close behavior are retained; no extra button, copy, loading state, or visual treatment is introduced. - **Leaving the chat is a navigation, not a reload.** 星盘 / 星历 / 我的报告 are @@ -782,7 +796,7 @@ Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小 校正面的所有等待复用行内等待:进入前的 hydration 在揭幕之前完成,进入后唯一的等待形态是时间线 live 行(含「正在准备下一个问题…」这一条独立 live 行)。打字回答的 live 行从发出那一帧起就是阶段句「收到,正在对照你的档案…」,按服务端 `turn.progress` 换成「正在记下这件事…」「正在重新对照盘面…」「正在准备下一个问题…」,属于流式生成中(BUG-1047)。区间交付卡只挂在最新那条采用旁白下面,不得留在更早的采集/区分题下。有未答的采集或选择题时卡仍在,「更像这个」置灰并写「先答完上面这道,再选时间」,不得整卡消失。卡上至多三列并排,相同性格句只写一次,点「更像这个」即采用该列分钟,按钮显示「正在采用…」或「已采用」。采用过程中整张卡留在原处,不得因 `busy` 卸掉。采用后前事核对结束走 `verified_idle`:一行收尾文案跟在卡片下面、与助手列对齐,没有 live 行、没有重载、没有采用状态条。卡片与右栏细则见 §11、§12。 -**每次打开网页只揭幕一次,客户端返回首页走暖快照(2026-09-26,BUG-1040)。** 整页加载(首次打开、刷新、登录后跳转)照旧放下面这一次加载屏。同一次打开里从星盘 / 星历 / 我的报告 / 星盘档案经侧栏「新建对话」、历史会话行、账户页脚或浏览器返回回到 `/`,不再出现加载环:第一次冷启动成功后,模型目录、校正入口摘要、会话分页游标和今日星语记进模块级暖快照(`lib/home-warm-snapshot.ts`,按账户隔离、只在内存、不落任何存储;账户、资料与会话列表本来就在布局层的会话列表 provider 里跨页存活)。首页重挂时若快照齐全,首帧即可用,落点按冷启动同一套规则同步算出(`?new=1` 新建本地空对话、`?c=` 打开该会话、其余一律当前人物的空白首页,同 BUG-1052),随后后台刷新模型目录、账户、后台回答恢复、入口摘要与今日星语,结果到了静默替换;消息没缓存的历史会话沿用首页内切换会话的留白方式。快照缺任何一项、落点需要去服务端查、或客户端导航没留下目标地址(Next 先渲染新页面、后写地址栏,`AppLink` 在点击时记下目标),一律回到冷启动,不半揭幕。账户变化、退出、任一 401 清空快照;今日星语跨日或换人按新键重取,卡片先用静态句。 +**每次打开网页只揭幕一次,客户端返回首页走暖快照(2026-09-26,BUG-1040)。** 整页加载(首次打开、刷新、登录后跳转)照旧放下面这一次加载屏。同一次打开里从星盘 / 星历 / 我的报告 / 星盘档案经侧栏「新建对话」、历史会话行、账户菜单的弹窗项(`/?account=`)或浏览器返回回到 `/`,不再出现加载环:第一次冷启动成功后,模型目录、校正入口摘要、会话分页游标和今日星语记进模块级暖快照(`lib/home-warm-snapshot.ts`,按账户隔离、只在内存、不落任何存储;账户、资料与会话列表本来就在布局层的会话列表 provider 里跨页存活)。首页重挂时若快照齐全,首帧即可用,落点按冷启动同一套规则同步算出(`?new=1` 新建本地空对话、`?c=` 打开该会话、其余一律当前人物的空白首页,同 BUG-1052),随后后台刷新模型目录、账户、后台回答恢复、入口摘要与今日星语,结果到了静默替换;消息没缓存的历史会话沿用首页内切换会话的留白方式。快照缺任何一项、落点需要去服务端查、或客户端导航没留下目标地址(Next 先渲染新页面、后写地址栏,`AppLink` 在点击时记下目标),一律回到冷启动,不半揭幕。账户变化、退出、任一 401 清空快照;今日星语跨日或换人按新键重取,卡片先用静态句。 **打开 `/` 落在哪里(2026-09-27,BUG-1052)。** 地址栏带 `?c=` 就进那条对话(分享链接、侧栏历史、在对话里刷新都靠它);带 `?new=1` 是新建对话;其余——登录后、手输网址、书签、刷新裸 `/`——一律是当前人物的空白首页(开场问候 + 输入框),已有一个没发过消息的新对话就复用它。不再记「上次在哪条对话」:离开首页、登录过期都不存任何返回记录,后台还在恢复的旧回答也不把空白首页切走。 diff --git a/frontend/src/app/(app)/page.tsx b/frontend/src/app/(app)/page.tsx index 40ed337f..46e018ab 100644 --- a/frontend/src/app/(app)/page.tsx +++ b/frontend/src/app/(app)/page.tsx @@ -46,7 +46,7 @@ import { composerDraftSnapshot, setComposerDraft } from "@/lib/composer-draft"; import { sessionMatchesSubject } from "@/lib/chat-profile-picker-model"; import { bindCurrentSubjectAccount, readCurrentSubjectId, useCurrentSubjectProfile } from "@/lib/current-subject"; import { DailyStarlanguageBinder } from "@/components/daily-starlanguage-binder"; -import { SynastryDeepLink } from "@/components/people-home-links"; +import { AccountDialogDeepLink, SynastryDeepLink } from "@/components/people-home-links"; import { StarterEntries, StarterGreeting } from "@/components/starter-home"; import { GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION, @@ -1163,6 +1163,7 @@ export default function Home() { setDailyStarlanguage={setDailyStarlanguage} /> + {replyAnnouncement} {pendingSessionDeletion ? (
setPendingSessionDeletion(null)}> diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index c96b7276..2aab0c88 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -16,9 +16,10 @@ import { Users, } from "lucide-react"; import { usePathname } from "next/navigation"; -import { useEffect, useRef } from "react"; -import type { Ref } from "react"; +import { useEffect, useRef, useState } from "react"; +import type { ReactNode, Ref } from "react"; import { AppLink } from "@/components/app-link"; +import { accountDialogHref, type AccountDialogIntent } from "@/lib/account-dialog-intent"; import { newChatHref, sessionHref } from "@/lib/chat-session-url"; import { prefetchSecondaryPage } from "@/lib/secondary-page-data"; import { prefetchBillingPanel } from "@/lib/billing-panel-loader"; @@ -57,9 +58,10 @@ export type SidebarAccount = { /** * Everything only `/` can supply: the session-management layer with its - * optimistic updates and rollback, and the account menu with the settings - * dialogs behind it. Omit `controls` and the same sidebar renders read-only — - * rows are links, the footer is a link, nothing writes. + * optimistic updates and rollback, and the settings dialogs behind the + * account menu. Omit `controls` and the same sidebar renders read-only — rows + * are links, nothing writes — and the same account menu's dialog entries + * become links to `/?account=` (TASK-self-edit-avatar-menu-20260928 S2). * * `/chart`, `/ephemeris` and `/reports` used to carry a second sidebar * component of their own, deleted in this round. Two components meant two sets @@ -133,12 +135,12 @@ export function AppSidebar({ Compiler rule treats every member of an object that carries a ref as a ref read during render, and `accountTriggerRef` lives in here. */ const { - accountMenuOpen = false, + accountMenuOpen: controlledMenuOpen = false, accountTriggerRef, newChatDisabled = false, creatingSession = false, sessionControls, - onAccountMenuOpenChange, + onAccountMenuOpenChange: controlledMenuOpenChange, onNewChat, onSelectSession, onOpenProfile, @@ -146,6 +148,10 @@ export function AppSidebar({ onOpenBilling, onOpenLogout, }: Partial = controls ?? {}; + /* Off `/` nothing controls the account menu, so it keeps its own open state (S2). */ + const [ownMenuOpen, setOwnMenuOpen] = useState(false); + const accountMenuOpen = controls ? controlledMenuOpen : ownMenuOpen; + const onAccountMenuOpenChange = controls ? controlledMenuOpenChange : setOwnMenuOpen; const hasMoreSessions = sessionControls?.hasMore ?? false; const onLoadMoreSessions = sessionControls?.onLoadMore; const favoriteSessions = sessions.filter((session) => session.pinned); @@ -228,6 +234,29 @@ export function AppSidebar({ ); } + /* One entry of the account menu: on `/` the dialog opens in place; on + another page the entry is a link to `/?account=` (S2). */ + function accountEntry( + dialog: AccountDialogIntent, + onOpen: (() => void) | undefined, + className: string, + content: ReactNode, + pointer: { onPointerEnter?: () => void; onPointerDown?: () => void } = {}, + ) { + return onOpen ? ( + {content} + ) : ( + } + {...pointer} + > + {content} + + ); + } + return ( @@ -333,7 +362,11 @@ export function AppSidebar({ - {controls && account ? ( + {account ? ( + /* One account menu on every page. On `/` Home controls it and its + entries open the dialogs in place; elsewhere it keeps its own open + state and the dialog entries go to `/?account=`, where Home + opens that dialog (S2). Theme applies where the reader is. */
- - - - - { prefetchBillingPanel(); prefetchPaymentPackages(); }} - onPointerDown={() => { prefetchBillingPanel(); prefetchPaymentPackages(); }} - onClick={onOpenBilling} - > - + {accountEntry("profile", onOpenProfile, "account-menu-item", ( + <>