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 {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
FileJson,
|
FileJson,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -127,6 +128,18 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchInput]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -151,119 +164,148 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
|||||||
return (
|
return (
|
||||||
// 配色与 AgentModuleSidebar 一致:浅色白底,深蓝主题半透明深蓝玻璃面板。
|
// 配色与 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">
|
<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}>
|
<div
|
||||||
<SelectTrigger className="h-9 w-full text-sm" title="切换智能体来源">
|
className={cn(
|
||||||
<SelectValue />
|
"flex flex-col",
|
||||||
</SelectTrigger>
|
agentsCollapsed ? "shrink-0" : "min-h-0 flex-[3]",
|
||||||
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
)}
|
||||||
<SelectItem value={MINE_SCOPE}>我的</SelectItem>
|
>
|
||||||
{squares.map((sq) => (
|
<button
|
||||||
<SelectItem key={sq.id} value={sq.id}>
|
type="button"
|
||||||
{sq.name}
|
onClick={toggleAgents}
|
||||||
</SelectItem>
|
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 ? "展开智能体" : "收起智能体"}
|
||||||
</SelectContent>
|
>
|
||||||
</Select>
|
<span>智能体</span>
|
||||||
<div className="relative">
|
<ChevronDown
|
||||||
<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
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-1 transition-opacity",
|
"text-muted-foreground size-4 shrink-0 transition-transform duration-200",
|
||||||
isFetching && "opacity-60",
|
agentsCollapsed && "-rotate-90",
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
{agents.map((agent) => {
|
</button>
|
||||||
const href = `${routeBase}/agents/${agent.id}/chats/new`;
|
|
||||||
const isActive = agent.id === activeAgentId;
|
{!agentsCollapsed && (
|
||||||
const { from, to } = agentColor(agent.id, isDark);
|
<>
|
||||||
return (
|
{/* 来源下拉(我的 / 各广场)+ 智能体名称模糊搜索 */}
|
||||||
<Link
|
<div className="shrink-0 space-y-2 border-b border-border/60 px-3 py-2.5">
|
||||||
key={agent.id}
|
<Select value={scope} onValueChange={setScope}>
|
||||||
href={href}
|
<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(
|
className={cn(
|
||||||
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
"flex flex-col gap-1 transition-opacity",
|
||||||
isActive
|
isFetching && "opacity-60",
|
||||||
? "border-primary/40 bg-primary/5"
|
|
||||||
: "border-transparent hover:border-border hover:bg-accent/50",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
{agents.map((agent) => {
|
||||||
className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-sm"
|
const href = `${routeBase}/agents/${agent.id}/chats/new`;
|
||||||
style={{
|
const isActive = agent.id === activeAgentId;
|
||||||
backgroundImage: `linear-gradient(135deg, ${from}, ${to})`,
|
const { from, to } = agentColor(agent.id, isDark);
|
||||||
}}
|
return (
|
||||||
>
|
<Link
|
||||||
<Bot className="size-[18px]" />
|
key={agent.id}
|
||||||
</span>
|
href={href}
|
||||||
<span className="min-w-0 flex-1">
|
className={cn(
|
||||||
<span
|
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 transition-colors",
|
||||||
className={cn(
|
isActive
|
||||||
"block truncate text-[13px] font-semibold",
|
? "border-primary/40 bg-primary/5"
|
||||||
isActive ? "text-primary" : "text-foreground",
|
: "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)}
|
共 {total} 个,查看全部 →
|
||||||
</span>
|
</Link>
|
||||||
<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>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
{/* 超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
|
</>
|
||||||
{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>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user