243 lines
9.6 KiB
TypeScript
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} />
|
|
)}
|
|
</>
|
|
);
|
|
}
|