Files
deerflow-code/frontend-web/src/components/workspace/agents/agent-selector.tsx
T

243 lines
9.6 KiB
TypeScript

"use client";
import { BotIcon, SettingsIcon, XIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { PromptInputButton } from "@/components/ai-elements/prompt-input";
import { AgentManageDialog } from "@/components/workspace/agents/agent-manage-dialog";
import { Tooltip } from "@/components/workspace/tooltip";
import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
import type { Agent } from "@/core/agents";
import { getStoredAuth } from "@/core/auth";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
interface AgentSelectorProps {
selectedAgentId: string | null;
onAgentChange: (agentId: string | null) => void;
disabled?: boolean;
/** 紧凑模式(CM 参考图):触发器改为灰底胶囊 + 主色机器人图标。 */
compact?: boolean;
/** 图标模式:用于 iframe 等底栏场景,仅显示机器人图标和可选清空按钮。 */
triggerVariant?: "default" | "icon";
iconButtonClassName?: string;
iconActiveClassName?: string;
}
/**
* Homepage agent picker — shows the admin-curated selector list.
*
* - Non-admins see a read-only dropdown.
* - Admins also see a "管理 Agent 列表" entry that opens the management dialog.
*
* Selecting an agent flips this chip to a coloured pill with a ✕ to clear.
* The actual handoff to ``AgentChatPage`` happens in ``ChatPage`` on submit.
*/
export function AgentSelector({
selectedAgentId,
onAgentChange,
disabled,
compact = false,
triggerVariant = "default",
iconButtonClassName,
iconActiveClassName,
}: AgentSelectorProps) {
const [open, setOpen] = useState(false);
const [manageOpen, setManageOpen] = useState(false);
const { agents, isLoading } = useAgentSelector();
// 弹窗列表只展示管理弹窗里配置的 agent(agents);但「已选中 agent」的名称解析要更宽——
// 侧边栏「常用智能体」可以选中未配置进管理弹窗的 agent(如用户自己的智能体),
// 这些 agent 也要能在胶囊上显示名称,故额外用「我的 / 广场」列表兜底解析(缓存与侧边栏共享)。
const { agents: mineAgents } = useAgentsPaginated({ scope: "mine", page: 1, pageSize: 20 });
const { agents: squareAgents } = useAgentsPaginated({ scope: "square", page: 1, pageSize: 20 });
const isAdmin = useMemo(() => getStoredAuth()?.system_role === "admin", []);
const selectedAgent = useMemo<Agent | null>(() => {
if (!selectedAgentId) return null;
return (
[...agents, ...mineAgents, ...squareAgents].find(
(a) => a.id === selectedAgentId,
) ?? null
);
}, [agents, mineAgents, squareAgents, selectedAgentId]);
const handleSelect = (agent: Agent) => {
onAgentChange(agent.id);
setOpen(false);
};
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
onAgentChange(null);
};
const triggerLabel = selectedAgent ? desensitize(selectedAgent.name) : "选择 Agent";
const iconTooltipLabel = selectedAgent ? triggerLabel : "未选择";
const iconMode = triggerVariant === "icon";
return (
<>
<Dialog open={open} onOpenChange={setOpen}>
{iconMode ? (
<Tooltip content={iconTooltipLabel}>
<div>
<DialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
disabled={disabled}
aria-label={selectedAgent ? `已选择智能体:${triggerLabel}` : "选择智能体"}
className={cn(
"relative size-9 rounded-full text-muted-foreground shadow-none hover:bg-muted/60 hover:text-foreground",
selectedAgent && "text-amber-600 hover:text-amber-700",
selectedAgent && iconActiveClassName,
iconButtonClassName,
)}
>
<BotIcon className="size-4" />
{selectedAgent && (
<span
role="button"
tabIndex={0}
aria-label="清除已选 agent"
className="absolute -right-1 -top-1 inline-flex size-4 items-center justify-center rounded-full border bg-background text-foreground shadow-sm hover:bg-muted"
onClick={handleClear}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onAgentChange(null);
}
}}
>
<XIcon className="size-2.5" />
</span>
)}
</Button>
</DialogTrigger>
</div>
</Tooltip>
) : (
<DialogTrigger asChild>
<PromptInputButton
type="button"
disabled={disabled}
className={cn(
"gap-1.5! rounded-full border px-3! shadow-none transition-colors",
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
compact
? "border-transparent bg-gray-50/60 text-gray-600 hover:bg-gray-100/80 hover:text-gray-800 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700"
: selectedAgent
? "border-[#d6a868] bg-[#fff3df] text-[#7a5417] hover:bg-[#ffe8c4]"
: "border-[#eee3d5] bg-[#fffaf5] text-[#7e6a57] hover:bg-white dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700",
)}
>
<BotIcon
className={cn("size-3", compact && "text-[var(--primary)]")}
/>
<span className="max-w-[140px] truncate text-xs font-normal">
{triggerLabel}
</span>
{selectedAgent && (
<span
role="button"
tabIndex={0}
aria-label="清除已选 agent"
className="ml-0.5 inline-flex items-center justify-center rounded-full p-0.5 hover:bg-[#ffd9a7]"
onClick={handleClear}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onAgentChange(null);
}
}}
>
<XIcon className="size-3" />
</span>
)}
</PromptInputButton>
</DialogTrigger>
)}
<DialogContent className="overflow-hidden p-0 sm:max-w-md">
<DialogTitle className="sr-only">选择 Agent</DialogTitle>
<Command className="**:data-[slot=command-input-wrapper]:h-auto">
<CommandInput placeholder="搜索 agent…" className="h-auto py-3.5" />
<CommandList>
<CommandEmpty>
{isLoading ? "加载中…" : "没有找到匹配的 agent"}
</CommandEmpty>
{agents.length > 0 && (
<CommandGroup>
{agents.map((agent) => (
<CommandItem
key={agent.id}
value={`${desensitize(agent.name)} ${desensitize(agent.description ?? "")}`}
onSelect={() => handleSelect(agent)}
className={cn(
"flex w-full min-w-0 items-start gap-2 overflow-hidden",
agent.id === selectedAgentId && "bg-accent",
)}
>
<BotIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm">{desensitize(agent.name)}</span>
{agent.description && (
<span className="line-clamp-2 whitespace-normal break-words text-[10px] leading-snug text-muted-foreground">
{desensitize(agent.description)}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
)}
{!isLoading && agents.length === 0 && (
<div className="px-4 py-6 text-center text-xs text-muted-foreground">
{isAdmin
? "还没有配置任何 agent。点击下方 “管理” 添加几个。"
: "管理员还没有配置可选的 agent。"}
</div>
)}
</CommandList>
{isAdmin && (
<div className="border-t px-2 py-1.5">
<Button
type="button"
size="sm"
variant="ghost"
className="w-full justify-start text-xs"
onClick={() => {
setOpen(false);
setManageOpen(true);
}}
>
<SettingsIcon className="mr-2 size-3.5" />
管理 Agent 列表 …
</Button>
</div>
)}
</Command>
</DialogContent>
</Dialog>
{isAdmin && (
<AgentManageDialog open={manageOpen} onOpenChange={setManageOpen} />
)}
</>
);
}