feat(agent-sidebar): 「智能体」区支持收起/展开
标题栏改为可折叠头(chevron 切换),收起后仅留标题栏并隐藏来源下拉/搜索/列表, 下方「最近的对话」占满剩余空间;收起状态持久化到 localStorage。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
8cc2650e98
commit
e906a911b2
1 file changed
+148
-106
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
Bot,
|
||||
ChevronDown,
|
||||
Download,
|
||||
FileJson,
|
||||
FileText,
|
||||
@@ -127,6 +128,18 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchInput]);
|
||||
|
||||
// 「智能体」区可收起(持久化);收起后只留标题栏,下方「最近的对话」占满剩余空间。
|
||||
const [agentsCollapsed, setAgentsCollapsed] = useState(
|
||||
() =>
|
||||
typeof window !== "undefined" &&
|
||||
localStorage.getItem("agent-chat-sidebar:agents-collapsed") === "true",
|
||||
);
|
||||
const toggleAgents = () =>
|
||||
setAgentsCollapsed((v) => {
|
||||
localStorage.setItem("agent-chat-sidebar:agents-collapsed", String(!v));
|
||||
return !v;
|
||||
});
|
||||
|
||||
// 若所选广场被管理员删除,回退到「我的」,避免指向不存在的广场。
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -151,119 +164,148 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
||||
return (
|
||||
// 配色与 AgentModuleSidebar 一致:浅色白底,深蓝主题半透明深蓝玻璃面板。
|
||||
<aside className="flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80">
|
||||
{/* 顶部:作用域下拉(我的 / 各广场)+ 智能体名称模糊搜索 */}
|
||||
<div className="shrink-0 space-y-2 border-b border-border/60 px-3 py-2.5">
|
||||
<Select value={scope} onValueChange={setScope}>
|
||||
<SelectTrigger className="h-9 w-full text-sm" title="切换智能体来源">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||
<SelectItem value={MINE_SCOPE}>我的</SelectItem>
|
||||
{squares.map((sq) => (
|
||||
<SelectItem key={sq.id} value={sq.id}>
|
||||
{sq.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
placeholder="搜索智能体名称"
|
||||
className="h-9 pl-8 pr-8 text-sm"
|
||||
aria-label="搜索智能体名称"
|
||||
/>
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchInput("")}
|
||||
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2"
|
||||
title="清除"
|
||||
aria-label="清除搜索"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 智能体列表(上半,独立滚动) */}
|
||||
<div className="flex min-h-0 flex-[3] flex-col overflow-y-auto px-2 py-3">
|
||||
<h2 className="mb-1 px-3 py-1 text-[11px] font-semibold tracking-[0.04em] text-muted-foreground/80 select-none">
|
||||
智能体
|
||||
</h2>
|
||||
{isLoading && agents.length === 0 ? (
|
||||
<div className="text-muted-foreground flex h-24 items-center justify-center gap-2 text-xs">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
加载中…
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<p className="px-3 py-1.5 text-xs text-muted-foreground/70">
|
||||
{search ? "未找到匹配的智能体" : isMine ? "暂无智能体" : "该广场暂无智能体"}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
{/* 智能体(上半,可收起)。展开时:标题栏 + 来源下拉/名称搜索 + 列表;
|
||||
收起时只留标题栏,下方「最近的对话」占满剩余空间。 */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
agentsCollapsed ? "shrink-0" : "min-h-0 flex-[3]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAgents}
|
||||
className="text-foreground/80 hover:bg-accent/40 flex shrink-0 items-center justify-between border-b border-border/60 px-3 py-2.5 text-[12px] font-semibold transition-colors select-none"
|
||||
title={agentsCollapsed ? "展开智能体" : "收起智能体"}
|
||||
>
|
||||
<span>智能体</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"flex flex-col gap-1 transition-opacity",
|
||||
isFetching && "opacity-60",
|
||||
"text-muted-foreground size-4 shrink-0 transition-transform duration-200",
|
||||
agentsCollapsed && "-rotate-90",
|
||||
)}
|
||||
>
|
||||
{agents.map((agent) => {
|
||||
const href = `${routeBase}/agents/${agent.id}/chats/new`;
|
||||
const isActive = agent.id === activeAgentId;
|
||||
const { from, to } = agentColor(agent.id, isDark);
|
||||
return (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={href}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!agentsCollapsed && (
|
||||
<>
|
||||
{/* 来源下拉(我的 / 各广场)+ 智能体名称模糊搜索 */}
|
||||
<div className="shrink-0 space-y-2 border-b border-border/60 px-3 py-2.5">
|
||||
<Select value={scope} onValueChange={setScope}>
|
||||
<SelectTrigger className="h-9 w-full text-sm" title="切换智能体来源">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||
<SelectItem value={MINE_SCOPE}>我的</SelectItem>
|
||||
{squares.map((sq) => (
|
||||
<SelectItem key={sq.id} value={sq.id}>
|
||||
{sq.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
placeholder="搜索智能体名称"
|
||||
className="h-9 pl-8 pr-8 text-sm"
|
||||
aria-label="搜索智能体名称"
|
||||
/>
|
||||
{searchInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchInput("")}
|
||||
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2"
|
||||
title="清除"
|
||||
aria-label="清除搜索"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 智能体列表(独立滚动) */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-2 py-3">
|
||||
{isLoading && agents.length === 0 ? (
|
||||
<div className="text-muted-foreground flex h-24 items-center justify-center gap-2 text-xs">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
加载中…
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<p className="px-3 py-1.5 text-xs text-muted-foreground/70">
|
||||
{search
|
||||
? "未找到匹配的智能体"
|
||||
: isMine
|
||||
? "暂无智能体"
|
||||
: "该广场暂无智能体"}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary/40 bg-primary/5"
|
||||
: "border-transparent hover:border-border hover:bg-accent/50",
|
||||
"flex flex-col gap-1 transition-opacity",
|
||||
isFetching && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-sm"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(135deg, ${from}, ${to})`,
|
||||
}}
|
||||
>
|
||||
<Bot className="size-[18px]" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate text-[13px] font-semibold",
|
||||
isActive ? "text-primary" : "text-foreground",
|
||||
)}
|
||||
{agents.map((agent) => {
|
||||
const href = `${routeBase}/agents/${agent.id}/chats/new`;
|
||||
const isActive = agent.id === activeAgentId;
|
||||
const { from, to } = agentColor(agent.id, isDark);
|
||||
return (
|
||||
<Link
|
||||
key={agent.id}
|
||||
href={href}
|
||||
className={cn(
|
||||
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary/40 bg-primary/5"
|
||||
: "border-transparent hover:border-border hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-sm"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(135deg, ${from}, ${to})`,
|
||||
}}
|
||||
>
|
||||
<Bot className="size-[18px]" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
"block truncate text-[13px] font-semibold",
|
||||
isActive ? "text-primary" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{desensitize(agent.name)}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] leading-snug text-muted-foreground">
|
||||
{agent.description
|
||||
? desensitize(agent.description)
|
||||
: "智能体助手"}
|
||||
</span>
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="absolute top-1/2 right-1 h-6 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{/* 超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
|
||||
{total > agents.length && (
|
||||
<Link
|
||||
href={`${routeBase}/agents`}
|
||||
className="text-muted-foreground hover:text-foreground hover:bg-accent/50 mt-0.5 rounded-lg px-3 py-1.5 text-center text-xs transition-colors"
|
||||
>
|
||||
{desensitize(agent.name)}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] leading-snug text-muted-foreground">
|
||||
{agent.description
|
||||
? desensitize(agent.description)
|
||||
: "智能体助手"}
|
||||
</span>
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="absolute top-1/2 right-1 h-6 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||
共 {total} 个,查看全部 →
|
||||
</Link>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{/* 超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
|
||||
{total > agents.length && (
|
||||
<Link
|
||||
href={`${routeBase}/agents`}
|
||||
className="text-muted-foreground hover:text-foreground mt-0.5 rounded-lg px-3 py-1.5 text-center text-xs transition-colors hover:bg-accent/50"
|
||||
>
|
||||
共 {total} 个,查看全部 →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user