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

898 lines
32 KiB
TypeScript

"use client";
import {
BotIcon,
CheckIcon,
GlobeIcon,
InfoIcon,
Loader2Icon,
LockIcon,
MessageCircleQuestionIcon,
PanelRightCloseIcon,
PanelRightOpenIcon,
PlusIcon,
ShieldIcon,
SparklesIcon,
SquarePenIcon,
Trash2Icon,
UploadCloudIcon,
UserRoundIcon,
XIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor";
import { useArtifactsOptional } from "@/components/workspace/artifacts";
import { EnabledSkillList } from "@/components/workspace/agents/enabled-skill-list";
import { SkillPickerSheet } from "@/components/workspace/agents/skill-picker-sheet";
import { PublishSquarePickerDialog } from "@/components/workspace/publish-square-picker-dialog";
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
import { Tooltip } from "@/components/workspace/tooltip";
import {
normalizeAgentQuestions,
useDeleteAgent,
useSetAgentFeatured,
useUpdateAgent,
type Agent,
type AgentQuestion,
type UpdateAgentRequest,
} from "@/core/agents";
import { getStoredAuth } from "@/core/auth";
import { isAgentSkillCandidate } from "@/core/skills";
import { useSkills } from "@/core/skills/hooks";
import { usePublishSquares } from "@/core/system-settings/hooks";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
/** Local edit-mode draft — committed in one `updateAgent` call on 保存. */
interface AgentDraft {
name: string;
description: string;
soul: string;
questions: AgentQuestion[];
skills: string[];
published: boolean;
featured: boolean;
templateName: string;
}
function draftFromAgent(agent: Agent): AgentDraft {
return {
name: agent.name,
description: agent.description ?? "",
soul: agent.soul ?? "",
questions: normalizeAgentQuestions(agent.common_questions),
skills: agent.skills ?? [],
published: agent.published,
featured: agent.featured_order != null,
templateName: agent.template_name ?? "",
};
}
function Section({
icon,
title,
extra,
children,
}: {
icon?: ReactNode;
title: string;
extra?: ReactNode;
children: ReactNode;
}) {
return (
<section className="bg-card/60 rounded-xl border p-4">
<div className="mb-3 flex items-center justify-between gap-2">
<h4 className="text-foreground flex items-center gap-1.5 text-sm font-semibold">
{icon}
{title}
</h4>
{extra}
</div>
{children}
</section>
);
}
/**
* 智能体详情面板 — the right-hand agent configuration center on the agent chat
* page. View mode shows the full profile (avatar, name, description, SOUL,
* 常用问题, enabled skills, publish/featured state); 管理员或发布者 can flip
* into an inline edit mode (no dialog) that edits everything locally and
* commits in a single 保存. 添加技能 opens a secondary drawer (SkillPickerSheet).
* Everyone else gets a read-only profile. On narrow screens (<lg) the panel
* collapses into a rail whose button opens the same content in a Sheet, so it
* never overlaps the chat input.
*/
export function AgentDetailPanel({
agent,
agentName,
onAskQuestion,
disabled,
}: {
agent: Agent | null | undefined;
agentName: string;
onAskQuestion: (text: string) => void;
disabled?: boolean;
}) {
const router = useRouter();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState<AgentDraft | null>(null);
const [editingQuestionIndex, setEditingQuestionIndex] = useState<
number | null
>(null);
const [questionDraft, setQuestionDraft] = useState<AgentQuestion>({
title: "",
prompt: "",
});
const [pickerOpen, setPickerOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [publishPickerOpen, setPublishPickerOpen] = useState(false);
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
const updateAgent = useUpdateAgent();
const deleteAgent = useDeleteAgent();
const setFeatured = useSetAgentFeatured();
const { data: publishSquares } = usePublishSquares();
const { skills, isLoading: isSkillsLoading } = useSkills();
// Permission model mirrors agent-card: admins manage the full catalog,
// owners manage their own agents, everyone else is read-only.
const storedAuth = useMemo(() => getStoredAuth(), []);
const currentUserId = storedAuth?.user_id ?? null;
const isAdmin = storedAuth?.system_role === "admin";
// 可配置的技能限定为「内置 + 自己的 + 已发布到广场的」,
// 其他用户未发布的自定义技能不可选(管理员例外)。
const enabledPool = useMemo(
() =>
skills.filter((skill) =>
isAgentSkillCandidate(skill, currentUserId, isAdmin),
),
[skills, currentUserId, isAdmin],
);
const isOwned =
!!agent &&
!agent.builtin &&
agent.user_id !== null &&
agent.user_id === currentUserId;
const canEdit = !!agent && (isOwned || isAdmin);
// Built-ins are inherently visible to everyone, so publish toggle is moot.
const canTogglePublish = !!agent && (isOwned || (isAdmin && !agent.builtin));
const isSaving = updateAgent.isPending || setFeatured.isPending;
// 对话生成文件(md/txt 等 artifact)并在右侧展开预览时,自动收起智能体
// 信息面板给预览让位;新文件落地时同样收起。编辑中不打断用户,用户随时
// 可手动重新展开。
const artifactsCtx = useArtifactsOptional();
const artifactCount = artifactsCtx?.artifacts.length ?? 0;
const artifactPanelVisible = !!artifactsCtx?.open && artifactCount > 0;
const prevArtifactPanelVisible = useRef(false);
const prevArtifactCount = useRef(0);
useEffect(() => {
const opened = artifactPanelVisible && !prevArtifactPanelVisible.current;
const grew =
artifactPanelVisible && artifactCount > prevArtifactCount.current;
prevArtifactPanelVisible.current = artifactPanelVisible;
prevArtifactCount.current = artifactCount;
if ((opened || grew) && !editing) {
setCollapsed(true);
}
}, [artifactPanelVisible, artifactCount, editing]);
const displayName = desensitize(agent?.name ?? agentName);
const questions = useMemo(
() => normalizeAgentQuestions(agent?.common_questions),
[agent?.common_questions],
);
function beginEdit() {
if (!agent || !canEdit) return;
cancelQuestionEdit();
setDraft(draftFromAgent(agent));
setEditing(true);
}
function cancelEdit() {
setEditing(false);
setDraft(null);
setPickerOpen(false);
}
function beginQuestionEdit(index: number, question: AgentQuestion) {
if (!canEdit) return;
setEditingQuestionIndex(index);
setQuestionDraft(question);
}
function cancelQuestionEdit() {
setEditingQuestionIndex(null);
setQuestionDraft({ title: "", prompt: "" });
}
async function saveQuestionEdit(index: number) {
if (!agent) return;
const nextQuestions = questions.map((question, questionIndex) =>
questionIndex === index
? {
title: questionDraft.title.trim() || questionDraft.prompt.trim(),
prompt: questionDraft.prompt.trim() || questionDraft.title.trim(),
}
: question,
);
const cleanedQuestions = nextQuestions.filter(
(question) => question.prompt.trim().length > 0,
);
try {
await updateAgent.mutateAsync({
id: agent.id,
request: { common_questions: cleanedQuestions },
});
toast.success("Updated");
cancelQuestionEdit();
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function saveEdit() {
if (!agent || !draft) return;
const trimmedName = draft.name.trim();
if (!trimmedName) {
toast.error("名称不能为空");
return;
}
// 私密广场与公开发布互斥 — force what the backend will accept.
const nextPublished = agent.is_private ? false : draft.published;
const nextQuestions = draft.questions
.map((q) => ({
title: q.title.trim() || q.prompt.trim(),
prompt: q.prompt.trim() || q.title.trim(),
}))
.filter((q) => q.prompt.length > 0);
// Only send the fields that actually changed: the backend merges partial
// updates (model_fields_set), so e.g. editing 常用问题 alone no longer
// re-saves skills/SOUL/发布状态 and the save returns quickly.
const request: UpdateAgentRequest = {};
if (trimmedName !== agent.name) request.name = trimmedName;
if (draft.description !== (agent.description ?? ""))
request.description = draft.description;
if (draft.soul !== (agent.soul ?? "")) request.soul = draft.soul;
if (draft.templateName.trim() !== (agent.template_name ?? ""))
request.template_name = draft.templateName.trim();
if (nextPublished !== agent.published) request.published = nextPublished;
if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? []))
request.skills = draft.skills;
if (
JSON.stringify(nextQuestions) !==
JSON.stringify(normalizeAgentQuestions(agent.common_questions))
)
request.common_questions = nextQuestions;
try {
if (Object.keys(request).length > 0) {
await updateAgent.mutateAsync({ id: agent.id, request });
}
// 首页推荐 rides a dedicated endpoint — only call it when toggled.
const wasFeatured = agent.featured_order != null;
if (isAdmin && draft.featured !== wasFeatured) {
if (draft.featured && !nextPublished) {
toast.warning("只有已发布的智能体可以加入首页推荐,已跳过该项");
} else {
await setFeatured.mutateAsync({
id: agent.id,
order: draft.featured ? Date.now() : null,
});
}
}
toast.success("智能体已更新");
setEditing(false);
setDraft(null);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleTogglePublished() {
if (!agent) return;
if (!agent.published && agent.is_private) {
toast.warning("该智能体已加入私密广场,不可发布");
return;
}
// Publishing: pick a target square when any are configured.
if (!agent.published && (publishSquares?.squares.length ?? 0) > 0) {
setPublishPickerOpen(true);
return;
}
try {
await updateAgent.mutateAsync({
id: agent.id,
request: { published: !agent.published },
});
toast.success(agent.published ? "已下架" : "已发布");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handlePublishToSquare(squareId: string) {
if (!agent) return;
try {
await updateAgent.mutateAsync({
id: agent.id,
request: { published: true, square_id: squareId },
});
const sqName =
publishSquares?.squares.find((s) => s.id === squareId)?.name ?? "广场";
toast.success(`已发布到「${sqName}」`);
setPublishPickerOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleDelete() {
if (!agent) return;
try {
await deleteAgent.mutateAsync(agent.id);
toast.success("智能体已删除");
setDeleteOpen(false);
router.push("/page/workspace/agents");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
const statusBadges = agent && (
<>
{agent.builtin && (
<Badge variant="secondary" className="text-xs">
内置
</Badge>
)}
{agent.published && !agent.is_private && (
<Badge
variant="outline"
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
>
<GlobeIcon className="size-3" />
已发布
</Badge>
)}
{!agent.published && !agent.builtin && (
<Badge variant="outline" className="text-muted-foreground text-xs">
未发布
</Badge>
)}
{agent.is_private && (
<Badge
variant="outline"
className="border-purple-500/30 text-purple-700 dark:text-purple-300 text-xs"
>
<ShieldIcon className="size-3" />
私密
</Badge>
)}
{agent.featured_order != null && (
<Badge
variant="outline"
className="border-amber-500/40 text-amber-700 dark:text-amber-300 text-xs"
>
<SparklesIcon className="size-3" />
首页推荐
</Badge>
)}
</>
);
const headerActions = canEdit ? (
editing ? (
<div className="flex items-center gap-1.5">
<Button size="sm" onClick={() => void saveEdit()} disabled={isSaving}>
{isSaving && <Loader2Icon className="size-3.5 animate-spin" />}
保存
</Button>
<Button
size="sm"
variant="outline"
onClick={cancelEdit}
disabled={isSaving}
>
取消
</Button>
</div>
) : (
<div className="flex items-center gap-1">
<Button size="sm" variant="outline" onClick={beginEdit}>
<SquarePenIcon className="size-3.5" />
编辑
</Button>
{canTogglePublish && (
<Tooltip content={agent?.published ? "下架" : "发布"}>
<Button
size="icon-sm"
variant="ghost"
onClick={() => void handleTogglePublished()}
disabled={updateAgent.isPending}
aria-label={agent?.published ? "下架" : "发布"}
>
{agent?.published ? (
<LockIcon className="size-3.5" />
) : (
<GlobeIcon className="size-3.5" />
)}
</Button>
</Tooltip>
)}
<Tooltip content="删除智能体">
<Button
size="icon-sm"
variant="ghost"
className="text-destructive hover:text-destructive"
onClick={() => setDeleteOpen(true)}
aria-label="删除智能体"
>
<Trash2Icon className="size-3.5" />
</Button>
</Tooltip>
</div>
)
) : null;
const skillNamesShown = editing && draft ? draft.skills : (agent?.skills ?? []);
const body = !agent ? (
<div className="text-muted-foreground flex h-40 items-center justify-center gap-2 text-sm">
<Loader2Icon className="size-4 animate-spin" />
智能体信息加载中…
</div>
) : (
<div className="space-y-4 px-4 py-4">
{/* ── Identity / 基础信息 ─────────────────────────────────────────── */}
<section className="bg-card/60 rounded-xl border p-4">
<div className="flex items-start gap-3">
<div className="bg-primary/10 ring-primary/15 flex size-12 shrink-0 items-center justify-center rounded-2xl ring-1">
<BotIcon className="text-primary size-6" />
</div>
<div className="min-w-0 flex-1 space-y-1.5">
{editing && draft ? (
<Input
value={draft.name}
onChange={(e) =>
setDraft({ ...draft, name: e.currentTarget.value })
}
placeholder="智能体名称"
className="h-8 text-sm font-semibold"
/>
) : (
<div
className="truncate text-base font-bold"
title={displayName}
>
{displayName}
</div>
)}
<div className="flex flex-wrap items-center gap-1">
{statusBadges}
</div>
{!editing && agent.owner_username && !agent.builtin && (
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<UserRoundIcon className="size-3 shrink-0" />
发布者:{desensitize(agent.owner_username)}
</div>
)}
</div>
</div>
<div className="mt-3">
{editing && draft ? (
<Textarea
value={draft.description}
onChange={(e) =>
setDraft({ ...draft, description: e.currentTarget.value })
}
placeholder="智能体描述"
className="min-h-16 text-sm"
/>
) : agent.description ? (
<p className="text-muted-foreground text-sm leading-relaxed">
{desensitize(agent.description)}
</p>
) : (
<p className="text-muted-foreground/60 text-sm">暂无描述</p>
)}
</div>
{/* 入库模板名 — view mode shows a hint row when configured. */}
{!editing && agent.template_name?.trim() && (
<div className="text-muted-foreground mt-2 flex items-center gap-1 text-xs">
<UploadCloudIcon className="size-3 shrink-0" />
入库模板:{desensitize(agent.template_name)}
</div>
)}
{/* Publish / featured toggles — only meaningful in edit mode. */}
{editing && draft && (
<div className="mt-3 space-y-2">
<div className="bg-background/60 space-y-1 rounded-lg border px-3 py-2">
<p className="text-sm font-medium">入库模板名</p>
<Input
value={draft.templateName}
onChange={(e) =>
setDraft({ ...draft, templateName: e.currentTarget.value })
}
placeholder="例如:临时策略库(留空表示不支持入库)"
className="h-8 text-sm"
/>
<p className="text-muted-foreground text-xs">
配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。
</p>
</div>
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
<div>
<p className="text-sm font-medium">公开发布</p>
<p className="text-muted-foreground text-xs">
{agent.is_private
? "已加入私密广场,不可发布"
: "打开后对所有用户可见"}
</p>
</div>
<Switch
checked={draft.published && !agent.is_private}
disabled={agent.is_private}
onCheckedChange={(v) => setDraft({ ...draft, published: v })}
/>
</div>
{isAdmin && (
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
<div>
<p className="text-sm font-medium">首页推荐</p>
<p className="text-muted-foreground text-xs">
加入首页选择器(需已发布)
</p>
</div>
<Switch
checked={draft.featured}
onCheckedChange={(v) => setDraft({ ...draft, featured: v })}
/>
</div>
)}
</div>
)}
</section>
{/* ── 常用问题 ───────────────────────────────────────────────────── */}
{(editing || questions.length > 0 || canEdit) && (
<Section
icon={<MessageCircleQuestionIcon className="size-3.5" />}
title="常用问题"
extra={
<div className="flex items-center gap-1">
<span className="text-muted-foreground text-xs tabular-nums">
{(editing && draft ? draft.questions : questions).length} 个
</span>
{!editing && canEdit && (
<Tooltip content="管理常用问题(新增 / 删除 / 排序)">
<Button
type="button"
size="icon-sm"
variant="ghost"
onClick={beginEdit}
aria-label="管理常用问题"
>
<SquarePenIcon className="size-3.5" />
</Button>
</Tooltip>
)}
</div>
}
>
{editing && draft ? (
<AgentQuestionEditor
value={draft.questions}
onChange={(next) => setDraft({ ...draft, questions: next })}
disabled={isSaving}
/>
) : questions.length === 0 ? (
<button
type="button"
onClick={beginEdit}
className="text-muted-foreground hover:text-foreground hover:border-border w-full rounded-lg border border-dashed px-3 py-4 text-center text-xs transition-colors"
>
<PlusIcon className="mr-1 inline size-3.5 align-[-2px]" />
添加常用问题
</button>
) : (
<div className="flex flex-col gap-2">
{questions.map((question, index) => {
const isEditingThis = editingQuestionIndex === index;
return (
<div
key={index}
className="border-border/60 bg-background/60 rounded-xl border p-2"
>
{isEditingThis ? (
<div className="space-y-2">
<Input
value={questionDraft.title}
disabled={isSaving}
placeholder="Title"
onChange={(e) =>
setQuestionDraft({
...questionDraft,
title: e.currentTarget.value,
})
}
className="h-8 text-sm font-medium"
/>
<Textarea
value={questionDraft.prompt}
disabled={isSaving}
placeholder="Prompt"
onChange={(e) =>
setQuestionDraft({
...questionDraft,
prompt: e.currentTarget.value,
})
}
className="min-h-16 resize-none text-sm"
/>
<div className="flex justify-end gap-1.5">
<Button
type="button"
size="sm"
variant="outline"
onClick={cancelQuestionEdit}
disabled={isSaving}
>
<XIcon className="size-3.5" />
Cancel
</Button>
<Button
type="button"
size="sm"
onClick={() => void saveQuestionEdit(index)}
disabled={isSaving}
>
{isSaving ? (
<Loader2Icon className="size-3.5 animate-spin" />
) : (
<CheckIcon className="size-3.5" />
)}
Save
</Button>
</div>
</div>
) : (
<div className="flex items-center gap-2">
<button
type="button"
disabled={disabled}
onClick={() => onAskQuestion(question.prompt)}
title={desensitize(question.prompt)}
className={cn(
"hover:bg-accent min-w-0 flex-1 rounded-lg px-2 py-1.5 text-left text-sm leading-relaxed transition-colors",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
>
<span className="block truncate">
{desensitize(question.title)}
</span>
</button>
{canEdit && (
<Button
type="button"
size="icon-sm"
variant="ghost"
onClick={() => beginQuestionEdit(index, question)}
disabled={isSaving}
aria-label="Edit question"
>
<SquarePenIcon className="size-3.5" />
</Button>
)}
</div>
)}
</div>
);
})}
</div>
)}
</Section>
)}
{/* ── 技能管理 ───────────────────────────────────────────────────── */}
<Section
icon={<SparklesIcon className="size-3.5" />}
title={`已启用技能 ${skillNamesShown.length} 个`}
extra={
editing ? (
<Button
type="button"
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => setPickerOpen(true)}
>
<PlusIcon className="size-3.5" />
添加技能
</Button>
) : undefined
}
>
<EnabledSkillList
skillNames={skillNamesShown}
skills={skills}
isLoading={isSkillsLoading}
editing={editing}
onRemove={
editing && draft
? (name) =>
setDraft({
...draft,
skills: draft.skills.filter((n) => n !== name),
})
: undefined
}
onViewDetail={setDetailSkillName}
/>
</Section>
</div>
);
const panelChrome = (collapseButton: ReactNode) => (
<>
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
<span className="text-sm font-semibold">智能体信息</span>
<div className="flex items-center gap-1">
{headerActions}
{collapseButton}
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">{body}</div>
</>
);
return (
<>
{/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */}
{collapsed ? (
<div className="hidden shrink-0 flex-col items-center border-l pt-3 lg:flex">
<Tooltip content="智能体信息">
<Button
variant="ghost"
size="icon-sm"
onClick={() => setCollapsed(false)}
aria-label="展开智能体信息"
>
<PanelRightOpenIcon className="size-4" />
</Button>
</Tooltip>
</div>
) : (
<aside className="bg-card/40 hidden h-full w-[360px] shrink-0 flex-col border-l lg:flex">
{panelChrome(
<Tooltip content="收起">
<Button
variant="ghost"
size="icon-sm"
onClick={() => setCollapsed(true)}
aria-label="收起智能体信息"
>
<PanelRightCloseIcon className="size-4" />
</Button>
</Tooltip>,
)}
</aside>
)}
{/* ── Narrow screens (<lg): rail trigger + drawer with the same body ── */}
<div className="flex shrink-0 flex-col items-center border-l pt-3 lg:hidden">
<Tooltip content="智能体信息">
<Button
variant="ghost"
size="icon-sm"
onClick={() => setMobileOpen(true)}
aria-label="打开智能体信息"
>
<InfoIcon className="size-4" />
</Button>
</Tooltip>
</div>
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-md">
<SheetHeader className="border-b px-4 pb-3 pt-4">
<SheetTitle className="flex items-center justify-between text-sm">
智能体信息
<span className="mr-6">{headerActions}</span>
</SheetTitle>
</SheetHeader>
<div className="min-h-0 flex-1 overflow-y-auto">{body}</div>
</SheetContent>
</Sheet>
{/* ── Secondary surfaces ───────────────────────────────────────────── */}
<SkillPickerSheet
open={pickerOpen}
onOpenChange={setPickerOpen}
skills={enabledPool}
isLoading={isSkillsLoading}
selected={editing && draft ? draft.skills : []}
onChange={(next) => {
if (editing && draft) setDraft({ ...draft, skills: next });
}}
onViewDetail={setDetailSkillName}
/>
<SkillDetailDialog
skillName={detailSkillName}
open={detailSkillName !== null}
onOpenChange={(o) => {
if (!o) setDetailSkillName(null);
}}
/>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>删除智能体</DialogTitle>
<DialogDescription>
确定要删除「{displayName}」吗?该操作不可恢复。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteOpen(false)}
disabled={deleteAgent.isPending}
>
取消
</Button>
<Button
variant="destructive"
onClick={() => void handleDelete()}
disabled={deleteAgent.isPending}
>
{deleteAgent.isPending ? "删除中…" : "删除"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<PublishSquarePickerDialog
open={publishPickerOpen}
onOpenChange={setPublishPickerOpen}
squares={publishSquares?.squares ?? []}
defaultSquareId={agent?.square_id || publishSquares?.default_square_id}
loading={updateAgent.isPending}
onConfirm={(squareId) => void handlePublishToSquare(squareId)}
description="请选择要把该智能体发布到哪个广场。每个智能体只能归属一个广场,不会重复出现。"
/>
</>
);
}