feat: zzJC/zzXD 左栏智能体列表对齐全量版彩色卡片样式

- 抽出共享配色助手 agent-icon-color.ts(按 id 散列、分浅色/深蓝主题),
  general-qa-sidebar 与 agent-module-sidebar 共用
- AgentModuleSidebar:背景改白底、去毛玻璃;智能体项改为彩色渐变图标卡片
  (图标+名称+两行简介+选中态主色描边/右侧主色条);分组标题与聊天记录配色统一
- 保留按类型分组/折叠与会话过滤逻辑

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-17 13:11:27 +08:00
1 parent 1c8b5b23a1
commit ef1a12ab74
4 files changed
+472 -149

No files matched your search

@@ -0,0 +1,35 @@
/**
* 智能体图标底色 —— 按 id 稳定散列到一组主题色,复刻参考图的彩色方块。
*
* 浅色模式沿用原高彩度彩色(不改);深蓝主题改用《深蓝 UI 规范》§1.1.7/§1.1.8 的蓝-青系
* (docs/ui规范-深蓝,克制、不刺眼),仅深蓝主题下生效,互不影响。
*
* 全量版通用问答左栏(GeneralQaSidebar)与 zzJC/zzXD 模块左栏(AgentModuleSidebar)共用,
* 保证彩色卡片配色完全一致。
*/
const AGENT_COLORS_LIGHT = [
{ from: "#fb7185", to: "#ef4444" },
{ from: "#60a5fa", to: "#3b82f6" },
{ from: "#fbbf24", to: "#f59e0b" },
{ from: "#34d399", to: "#10b981" },
{ from: "#a78bfa", to: "#8b5cf6" },
] as const;
const AGENT_COLORS_DARK = [
{ from: "#4096FF", to: "#1677FF" }, // 蓝(链接蓝 → 主色)
{ from: "#36CFC9", to: "#13C2C2" }, // 青(辅色)
{ from: "#6388FF", to: "#0958D9" }, // 紫蓝(装饰紫 → 主色深一档)
{ from: "#5CDBD3", to: "#08979C" }, // 浅青 → 深青
{ from: "#69B1FF", to: "#003EB3" }, // 浅蓝 → 主色按下
] as const;
/** 按 agent id 稳定散列出一对渐变色(from → to)。`isDark` 切换深蓝/浅色调色板。 */
export function agentColor(id: string, isDark: boolean) {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
}
const palette = isDark ? AGENT_COLORS_DARK : AGENT_COLORS_LIGHT;
return palette[hash % palette.length]!;
}
@@ -5,6 +5,8 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { agentColor } from "@/components/workspace/agent-icon-color";
import { useTheme } from "@/contexts/ThemeContext";
import { useAgents } from "@/core/agents";
import type { Agent } from "@/core/agents";
import { useAgentModuleConfig } from "@/core/system-settings/hooks";
@@ -125,6 +127,9 @@ export function AgentModuleSidebar({
}) {
const routeBase = useRouteBase();
const pathname = usePathname();
// 智能体图标配色分主题:深蓝主题用规范蓝-青系,其它(浅色)保持原高彩度色,互不影响。
const { theme } = useTheme();
const isDark = theme === "dark";
const { agents } = useAgents();
const { data: threads = [] } = useThreads();
const { data: moduleConfig } = useAgentModuleConfig(moduleKey);
@@ -165,7 +170,8 @@ export function AgentModuleSidebar({
});
return (
<aside className="flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-card/60 backdrop-blur-sm">
// 背景按需求固定白色(不走 sidebar token),与全量版 GeneralQaSidebar 的卡片视觉看齐。
<aside className="flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground">
{onOpenSettings && (
<div className="flex shrink-0 items-center justify-between border-b border-border/60 px-3 py-2">
<span className="text-xs font-semibold text-foreground/80">智能体</span>
@@ -174,22 +180,22 @@ export function AgentModuleSidebar({
onClick={onOpenSettings}
title="模块设置"
aria-label="模块设置"
className="text-muted-foreground hover:text-foreground rounded-md p-1 transition-colors hover:bg-sidebar-accent/60"
className="text-muted-foreground hover:text-foreground rounded-md p-1 transition-colors hover:bg-accent/60"
>
<Settings className="size-4" />
</button>
</div>
)}
{/* 上半:按类型分组的智能体 */}
{/* 上半:按类型分组的智能体(彩色卡片,与全量版看齐) */}
<div className="flex min-h-0 flex-[3] flex-col overflow-y-auto px-2 py-3">
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
<h2 className="mb-1 px-3 py-1 text-[11px] font-semibold tracking-[0.04em] text-muted-foreground/80 select-none">
智能体类型
</h2>
{groups.length === 0 ? (
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无智能体</p>
<p className="px-3 py-1.5 text-xs text-muted-foreground/70">暂无智能体</p>
) : (
<div className="flex flex-col gap-0.5">
<div className="flex flex-col gap-1.5">
{groups.map((group) => {
const isCollapsed = collapsed.has(group.key);
return (
@@ -197,11 +203,11 @@ export function AgentModuleSidebar({
<button
type="button"
onClick={() => toggleGroup(group.key)}
className="flex w-full items-center justify-between gap-1 rounded-lg px-2 py-1.5 text-xs font-semibold text-foreground/80 hover:bg-sidebar-accent/60"
className="flex w-full items-center justify-between px-3 py-1 text-[11px] font-semibold tracking-[0.04em] text-muted-foreground/80 transition-colors hover:text-muted-foreground select-none"
>
<span className="flex items-center gap-1.5 truncate">
<span className="truncate">{group.label}</span>
<span className="text-muted-foreground/60 text-[10px] font-normal tabular-nums">
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground tabular-nums">
{group.agents.length}
</span>
</span>
@@ -213,22 +219,48 @@ export function AgentModuleSidebar({
/>
</button>
{!isCollapsed && (
<div className="mt-0.5 flex flex-col gap-0.5 pl-1">
<div className="mt-1 flex flex-col gap-1">
{group.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={`${group.key}:${agent.id}`}
href={href}
className={cn(
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/60",
isActive &&
"bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary",
"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",
)}
>
<Bot className="size-3.5 shrink-0 text-muted-foreground/70" />
<span className="truncate">{desensitize(agent.name)}</span>
<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>
);
})}
@@ -241,13 +273,13 @@ export function AgentModuleSidebar({
)}
</div>
{/* 下半:聊天记录(仅本模块智能体会话) */}
{/* 下半:聊天记录(仅本模块智能体会话),简单列表、配色与全量版统一 */}
<div className="flex min-h-0 flex-[2] flex-col overflow-y-auto border-t border-border/60 px-2 py-3">
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
<h2 className="mb-1 px-3 py-1 text-[11px] font-semibold tracking-[0.04em] text-muted-foreground/80 select-none">
聊天记录
</h2>
{moduleThreads.length === 0 ? (
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无聊天记录</p>
<p className="px-3 py-1.5 text-xs text-muted-foreground/70">暂无聊天记录</p>
) : (
<div className="flex flex-col gap-0.5">
{moduleThreads.map((thread) => {
@@ -258,15 +290,24 @@ export function AgentModuleSidebar({
key={thread.thread_id}
href={href}
className={cn(
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/60",
isActive &&
"bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary",
"relative flex h-9 min-w-0 items-center gap-2 rounded-lg px-2.5 text-[13px] transition-colors",
isActive
? "border border-primary/40 bg-primary/5 font-medium text-primary"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
)}
>
<MessageSquare className="size-3.5 shrink-0 text-muted-foreground/70" />
<MessageSquare
className={cn(
"size-3.5 shrink-0",
isActive ? "text-primary" : "text-muted-foreground/70",
)}
/>
<span className="truncate">
{desensitize(titleOfThread(thread, "未命名对话"))}
</span>
{isActive && (
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
)}
</Link>
);
})}
@@ -86,6 +86,7 @@ import { desensitize } from "@/lib/desensitize";
import { isIMEComposing } from "@/lib/ime";
import { cn } from "@/lib/utils";
import { agentColor } from "./agent-icon-color";
import { SidebarCollapseButton } from "./workspace-header";
const NEW_CHAT_PATH = "/page/workspace/chats/new";
@@ -96,33 +97,8 @@ const MORE_CHATS_HREF = "/page/workspace/chats";
/** 常用智能体最多展示个数。 */
const MAX_FREQUENT = 4;
// 智能体图标统一用 Bot 机器人图标;底色按 id 稳定散列到一组主题色,复刻参考图的彩色方块。
// 浅色模式沿用原高彩度彩色(不改);深蓝主题改用《深蓝 UI 规范》§1.1.7/§1.1.8 的蓝-青系
// (docs/ui规范-深蓝,克制、不刺眼),仅深蓝主题下生效,互不影响。
const AGENT_COLORS_LIGHT = [
{ from: "#fb7185", to: "#ef4444" },
{ from: "#60a5fa", to: "#3b82f6" },
{ from: "#fbbf24", to: "#f59e0b" },
{ from: "#34d399", to: "#10b981" },
{ from: "#a78bfa", to: "#8b5cf6" },
] as const;
const AGENT_COLORS_DARK = [
{ from: "#4096FF", to: "#1677FF" }, // 蓝(链接蓝 → 主色)
{ from: "#36CFC9", to: "#13C2C2" }, // 青(辅色)
{ from: "#6388FF", to: "#0958D9" }, // 紫蓝(装饰紫 → 主色深一档)
{ from: "#5CDBD3", to: "#08979C" }, // 浅青 → 深青
{ from: "#69B1FF", to: "#003EB3" }, // 浅蓝 → 主色按下
] as const;
function agentColor(id: string, isDark: boolean) {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
}
const palette = isDark ? AGENT_COLORS_DARK : AGENT_COLORS_LIGHT;
return palette[hash % palette.length]!;
}
// 智能体图标配色(按 id 稳定散列、分主题)抽到共享模块 ./agent-icon-color,
// 与 zzJC/zzXD 模块左栏(AgentModuleSidebar)共用,保证彩色卡片配色完全一致。
/** 折叠区块标题(带 chevron 与可选计数徽标)。 */
function SectionLabel({