fix(ui): 回答不再折叠,次级页侧栏改真链接并补回头像
真机走查第二批: 1. 普通对话的回答,首个 H2 之后的全部内容(带推理的那一半)被 <details class="answer-detail">「完整分析」默认收起——用户「输出根本 看不到」。折叠控件与组件文件一并删除,报告层直接渲染进正文。 技法审计表保留自己的折叠:那是给人核对的证据,不是回复正文。 product-voice.ts 同步改掉「UI 会折叠」那句,否则模型继续按折叠写。 2. 次级页侧栏点不了头像、头像样式与首页不一致。根因是 R4 的取舍: SecondaryShell 共用,但里面挂的是新写的 AppNavRail 而不是 AppSidebar。 现在 use-nav-rail 也取 avatar,渲染同一个 UserAvatar;页脚改成真链接, 指向 / —— 账户菜单连着设置弹窗栈,留在那儿,不在这里复制第二份。 3. 「新建对话」整页刷新。原来走 location.assign,现在是 next/link。 /login 保留硬跳转:它跨鉴权边界,要先存返回目标。 注意:第一版我改成了 useRouter(),它在 app-router 上下文外会抛 「invariant expected app router to be mounted」,打红 12 条渲染测试。 改用 <Link>,静态渲染也安全;折叠态 tooltip 换成原生 title。 测试 3369,fail 仍 31 且与基线逐条一致;四个路由标记不变; CSS gzip 40,002。 未修:截图里 `## 适合推进 / 需要避开` 被当字面量渲染,是模型输出里 那两个 H2 前面没有换行,属生成层,另记。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
co-authored by
Claude Opus 5
parent
49bc4a8ba1
commit
524015cfc1
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { CalendarDays, FileText, Orbit, SquarePen, Star } from "lucide-react";
|
||||
import {
|
||||
@@ -10,13 +11,13 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { persistLoginSessionReturn, sessionHref } from "@/lib/chat-session-url";
|
||||
import { groupSessionsByRecency } from "@/lib/session-groups";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { useNavRail, type NavRailSession } from "@/hooks/use-nav-rail";
|
||||
|
||||
/**
|
||||
@@ -47,24 +48,26 @@ export function AppNavRail() {
|
||||
const history = sessions.filter((session) => !session.pinned);
|
||||
const groups = groupSessionsByRecency(history);
|
||||
|
||||
/* Same navigation the chat sidebar performs when it leaves for one of these
|
||||
pages: a real document load, with the return target stored first. */
|
||||
function go(href: string) {
|
||||
/* Real links, not buttons with a router call. `useRouter()` throws outside an
|
||||
app-router context (it took out twelve render tests), and an <a href> gives
|
||||
client-side navigation for free — clicking 新建对话 from a chart used to
|
||||
reload the whole document. `/login` keeps the hard exit: it crosses an auth
|
||||
boundary and wants the return target persisted first. */
|
||||
function leaveForLogin() {
|
||||
persistLoginSessionReturn();
|
||||
if (isMobile) setOpenMobile(false);
|
||||
window.location.assign(href);
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
if (isMobile) setOpenMobile(false);
|
||||
}
|
||||
|
||||
function renderRow(session: NavRailSession) {
|
||||
return (
|
||||
<SidebarMenuItem key={session.id}>
|
||||
<button
|
||||
className="session-row nav-rail-row"
|
||||
type="button"
|
||||
onClick={() => go(sessionHref("", session.id))}
|
||||
>
|
||||
<Link className="session-row nav-rail-row" href={sessionHref("", session.id)} onClick={closeDrawer}>
|
||||
<span className="session-title">{session.title}</span>
|
||||
</button>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
@@ -78,23 +81,24 @@ export function AppNavRail() {
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="new-chat" type="button" tooltip="新对话" onClick={() => go("/")}>
|
||||
<Link className="new-chat" data-sidebar="menu-button" href="/" title="新对话" onClick={closeDrawer}>
|
||||
<SquarePen size={18} strokeWidth={1.75} aria-hidden="true" />
|
||||
{showExpandedContent ? <span>新建对话</span> : null}
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
{NAV_PAGES.map(({ href, label, icon: Icon }) => (
|
||||
<SidebarMenuItem key={href}>
|
||||
<SidebarMenuButton
|
||||
<Link
|
||||
className="report-nav-button"
|
||||
type="button"
|
||||
tooltip={label}
|
||||
isActive={pathname === href || pathname.startsWith(`${href}/`)}
|
||||
onClick={() => go(href)}
|
||||
data-sidebar="menu-button"
|
||||
data-active={pathname === href || pathname.startsWith(`${href}/`)}
|
||||
href={href}
|
||||
title={label}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<Icon size={18} strokeWidth={1.75} aria-hidden="true" />
|
||||
{showExpandedContent ? <span>{label}</span> : null}
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
@@ -136,22 +140,32 @@ export function AppNavRail() {
|
||||
|
||||
<SidebarFooter className="sidebar-footer">
|
||||
{signedOut ? (
|
||||
<button className="nav-rail-identity" type="button" onClick={() => go("/login")}>
|
||||
<a className="nav-rail-identity" href="/login" onClick={leaveForLogin}>
|
||||
<span className="profile-initial" aria-hidden="true">·</span>
|
||||
{showExpandedContent ? <span><b>去登录</b></span> : null}
|
||||
</button>
|
||||
</a>
|
||||
) : (
|
||||
/* Identity only. Account actions live in the chat page's own menu; a
|
||||
second copy here would need the settings dialog stack to come with it. */
|
||||
<div className="nav-rail-identity" aria-label={account ? `${account.name},余额 ${account.credits} 点` : undefined}>
|
||||
<span className="profile-initial" aria-hidden="true">{account?.initial ?? "·"}</span>
|
||||
/* Draws the same beam avatar the chat sidebar does — the rail used to
|
||||
show only the initial, so the same account wore two different faces
|
||||
depending on which page you were on. It is a real control now: the
|
||||
account menu itself lives on `/` with the settings dialogs behind
|
||||
it, so this goes there rather than growing a second copy. */
|
||||
<Link
|
||||
className="nav-rail-identity"
|
||||
href="/"
|
||||
aria-label={account ? `${account.name},余额 ${account.credits} 点,打开账户` : "打开账户"}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{account?.avatar
|
||||
? <UserAvatar avatar={account.avatar} size={32} className="profile-avatar" />
|
||||
: <span className="profile-initial" aria-hidden="true">{account?.initial ?? "·"}</span>}
|
||||
{showExpandedContent && account ? (
|
||||
<>
|
||||
<span><b>{account.name}</b></span>
|
||||
<small>{account.credits} 点</small>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { answerDetailCaption } from "@/lib/chat-answer-split";
|
||||
|
||||
export function AnswerDetailDisclosure({
|
||||
headings,
|
||||
streaming = false,
|
||||
children,
|
||||
}: {
|
||||
readonly headings: readonly string[];
|
||||
readonly streaming?: boolean;
|
||||
readonly children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<details className="answer-detail">
|
||||
<summary>
|
||||
完整分析
|
||||
<small>{streaming ? "正在写…" : answerDetailCaption(headings)}</small>
|
||||
</summary>
|
||||
{streaming ? null : <div className="answer-detail-panel">{children}</div>}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { memo, useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { AnswerDetailDisclosure } from "@/components/chat-answer-detail";
|
||||
import { prefetchOnIdle } from "@/components/chat-chunk-prefetch";
|
||||
import { plainParagraphs } from "@/components/chat-message-paragraphs";
|
||||
import { TechniqueAuditDisclosure } from "@/components/technique-audit-disclosure";
|
||||
@@ -139,13 +138,12 @@ export function ChatMessageContent({
|
||||
: <StableMarkdownPrefix text={spoken} renderMarkdown={renderMarkdown} />}
|
||||
</div>
|
||||
) : null}
|
||||
{report
|
||||
? (
|
||||
<AnswerDetailDisclosure headings={layers.headings} streaming={streaming}>
|
||||
{reportBody}
|
||||
</AnswerDetailDisclosure>
|
||||
)
|
||||
: foldedAudit}
|
||||
{/* The body used to sit inside a collapsed 完整分析 control: everything
|
||||
from the first H2 down was one click away, so the part of the answer
|
||||
that carries the reasoning was invisible by default. The answer is the
|
||||
product — it renders inline. The technique audit keeps its own
|
||||
disclosure, because that one is evidence for checking, not the reply. */}
|
||||
{report ? <div className="answer-report-body">{reportBody}</div> : foldedAudit}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user