Merge origin/main into agent-guided birth time rectification

This commit is contained in:
Jesse_Chen
2026-07-18 18:54:21 +08:00
170 changed files with 77128 additions and 355 deletions
+28 -17
View File
@@ -28,12 +28,11 @@ import {
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
export type SidebarSession = {
id: string;
title: string;
messageCount: number;
};
import {
SidebarSessionRow,
type SidebarSession,
type SidebarSessionControls,
} from "@/components/sidebar-session-row";
export type SidebarAccount = {
name: string;
@@ -51,6 +50,7 @@ export type AppSidebarProps = {
accountTriggerRef: Ref<HTMLButtonElement>;
newChatDisabled: boolean;
creatingSession: boolean;
sessionControls: SidebarSessionControls;
onAccountMenuOpenChange: (open: boolean) => void;
onNewChat: () => void;
onSelectSession: (sessionId: string) => void;
@@ -67,6 +67,7 @@ export function AppSidebar({
accountTriggerRef,
newChatDisabled,
creatingSession,
sessionControls,
onAccountMenuOpenChange,
onNewChat,
onSelectSession,
@@ -133,26 +134,36 @@ export function AppSidebar({
<SidebarContent>
{showExpandedContent ? (
<SidebarGroup className="session-nav" aria-label="聊天记录">
<SidebarGroupLabel className="sidebar-label" ref={historyHeadingRef} tabIndex={-1}></SidebarGroupLabel>
<div className="session-nav-header">
<SidebarGroupLabel className="sidebar-label" ref={historyHeadingRef} tabIndex={-1}>
{sessionControls.showingArchived ? "归档记录" : "聊天记录"}
</SidebarGroupLabel>
<button className="session-nav-toggle" type="button" onClick={sessionControls.onToggleArchivedView}>
{sessionControls.showingArchived ? "返回" : `归档 ${sessionControls.archivedCount}`}
</button>
</div>
<SidebarGroupContent>
{sessions.length === 0 ? <p className="sidebar-empty"></p> : (
<SidebarMenu className="session-list">
{sessions.map((session, index) => (
<SidebarMenuItem key={session.id}>
<SidebarMenuButton
<SidebarSessionRow
ref={index === 0 ? firstSessionRef : undefined}
className="session-row"
type="button"
isActive={session.id === activeSessionId}
aria-current={session.id === activeSessionId ? "page" : undefined}
onClick={() => {
session={session}
active={session.id === activeSessionId}
disabled={sessionControls.disabled}
menuOpen={sessionControls.menuSessionId === session.id}
onMenuOpenChange={(open) => sessionControls.onMenuSessionChange(open ? session.id : null)}
onSelect={() => {
onSelectSession(session.id);
if (isMobile) setOpenMobile(false);
}}
>
<span className="truncate">{session.title}</span>
{session.messageCount > 0 ? <small>{session.messageCount} </small> : null}
</SidebarMenuButton>
onTogglePinned={() => sessionControls.onTogglePinned(session.id)}
onRename={() => sessionControls.onRename(session.id)}
onShare={() => sessionControls.onShare(session.id)}
onToggleArchived={() => sessionControls.onToggleArchived(session.id)}
onDelete={() => sessionControls.onDelete(session.id)}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
@@ -0,0 +1,120 @@
"use client";
import {
Archive,
ArchiveRestore,
MoreHorizontal,
Pencil,
Pin,
PinOff,
Share2,
Trash2,
} from "lucide-react";
import { forwardRef } from "react";
import { SidebarMenuButton } from "@/components/ui/sidebar";
export type SidebarSession = {
readonly id: string;
readonly title: string;
readonly messageCount: number;
readonly pinned: boolean;
readonly archived: boolean;
};
export type SidebarSessionControls = {
readonly archivedCount: number;
readonly showingArchived: boolean;
readonly menuSessionId: string | null;
readonly disabled: boolean;
readonly onToggleArchivedView: () => void;
readonly onMenuSessionChange: (sessionId: string | null) => void;
readonly onTogglePinned: (sessionId: string) => void;
readonly onRename: (sessionId: string) => void;
readonly onShare: (sessionId: string) => void;
readonly onToggleArchived: (sessionId: string) => void;
readonly onDelete: (sessionId: string) => void;
};
type SidebarSessionRowProps = {
readonly session: SidebarSession;
readonly active: boolean;
readonly disabled: boolean;
readonly menuOpen: boolean;
readonly onMenuOpenChange: (open: boolean) => void;
readonly onSelect: () => void;
readonly onTogglePinned: () => void;
readonly onRename: () => void;
readonly onShare: () => void;
readonly onToggleArchived: () => void;
readonly onDelete: () => void;
};
export const SidebarSessionRow = forwardRef<HTMLButtonElement, SidebarSessionRowProps>(function SidebarSessionRow({
session,
active,
disabled,
menuOpen,
onMenuOpenChange,
onSelect,
onTogglePinned,
onRename,
onShare,
onToggleArchived,
onDelete,
}, ref) {
function runAction(action: () => void) {
onMenuOpenChange(false);
action();
}
return (
<div
className="session-row"
onContextMenu={(event) => {
event.preventDefault();
if (!disabled) onMenuOpenChange(!menuOpen);
}}
>
<SidebarMenuButton
ref={ref}
className="session-main"
type="button"
isActive={active}
aria-current={active ? "page" : undefined}
disabled={disabled}
onClick={onSelect}
>
<span className="session-title">
{session.pinned ? <Pin aria-label="已置顶" /> : null}
<span className="truncate">{session.title}</span>
</span>
{session.messageCount > 0 ? <small>{session.messageCount} </small> : null}
</SidebarMenuButton>
<button
className="session-menu-trigger"
type="button"
aria-label={`${session.title} 更多操作`}
aria-expanded={menuOpen}
disabled={disabled}
onClick={() => onMenuOpenChange(!menuOpen)}
>
<MoreHorizontal aria-hidden="true" />
</button>
{menuOpen ? (
<div className="session-actions" role="menu" aria-label={`${session.title} 操作`}>
<button type="button" role="menuitem" onClick={() => runAction(onTogglePinned)}>
{session.pinned ? <PinOff aria-hidden="true" /> : <Pin aria-hidden="true" />}
<span>{session.pinned ? "取消置顶" : "置顶"}</span>
</button>
<button type="button" role="menuitem" onClick={() => runAction(onRename)}><Pencil aria-hidden="true" /><span></span></button>
<button type="button" role="menuitem" onClick={() => runAction(onShare)}><Share2 aria-hidden="true" /><span></span></button>
<button type="button" role="menuitem" onClick={() => runAction(onToggleArchived)}>
{session.archived ? <ArchiveRestore aria-hidden="true" /> : <Archive aria-hidden="true" />}
<span>{session.archived ? "恢复" : "归档"}</span>
</button>
<button className="session-action-danger" type="button" role="menuitem" onClick={() => runAction(onDelete)}><Trash2 aria-hidden="true" /><span></span></button>
</div>
) : null}
</div>
);
});