wip: 合并 master 前暂存本地改动
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1ce15fbb15
commit
44c0f6189b
13 files changed
+712
-444
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
LockIcon,
|
||||
MessageSquareIcon,
|
||||
PinIcon,
|
||||
SearchIcon,
|
||||
ShieldIcon,
|
||||
SparklesIcon,
|
||||
SquarePenIcon,
|
||||
@@ -74,6 +75,9 @@ interface AgentCardProps {
|
||||
/** "card" (default) renders the full gallery card; "list" renders a compact
|
||||
* single-row layout for the list view. All dialogs/actions are shared. */
|
||||
layout?: "card" | "list";
|
||||
/** Click the 发布者 line to filter the gallery to that publisher. Receives the
|
||||
* owner user_id + its desensitized display label. */
|
||||
onPublisherClick?: (publisher: { id: string; label: string }) => void;
|
||||
}
|
||||
|
||||
interface BadgeItem {
|
||||
@@ -185,7 +189,7 @@ function ClampedBadgeRow({ items }: { items: BadgeItem[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCardProps) {
|
||||
const isList = layout === "list";
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -208,6 +212,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>(
|
||||
agent.skills ?? [],
|
||||
);
|
||||
const [skillSearch, setSkillSearch] = useState("");
|
||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||
|
||||
// The /api/agents list response intentionally omits `soul` (perf optimization
|
||||
@@ -245,6 +250,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
setEditPublished(agent.published);
|
||||
setEditIsPrivate(agent.is_private);
|
||||
setSelectedSkills(agent.skills ?? []);
|
||||
setSkillSearch("");
|
||||
}, [
|
||||
agent.description,
|
||||
agent.is_private,
|
||||
@@ -437,7 +443,24 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
});
|
||||
}
|
||||
|
||||
const enabledSkills = skills.filter((skill) => skill.enabled);
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((skill) => skill.enabled),
|
||||
[skills],
|
||||
);
|
||||
|
||||
// Search over English name, Chinese name (name_zh) and description, so both
|
||||
// 中英文 work. Already-selected skills stay searchable too (selection is
|
||||
// independent of this filter).
|
||||
const filteredSkills = useMemo(() => {
|
||||
const q = skillSearch.trim().toLowerCase();
|
||||
if (!q) return enabledSkills;
|
||||
return enabledSkills.filter(
|
||||
(skill) =>
|
||||
skill.name.toLowerCase().includes(q) ||
|
||||
(skill.name_zh ?? "").toLowerCase().includes(q) ||
|
||||
(skill.description ?? "").toLowerCase().includes(q),
|
||||
);
|
||||
}, [enabledSkills, skillSearch]);
|
||||
|
||||
// Status badges (内置 / 我的 / 来自他人 / 已发布 / 私密 / 首页推荐) — shared by
|
||||
// both the card header and the compact list row.
|
||||
@@ -498,16 +521,36 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
</>
|
||||
);
|
||||
|
||||
// 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。
|
||||
// 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。点击可按发布者筛选。
|
||||
const publisherLabel = agent.owner_username
|
||||
? desensitize(agent.owner_username)
|
||||
: "";
|
||||
const canFilterByPublisher =
|
||||
!isBuiltin && !!agent.owner_username && !!agent.user_id && !!onPublisherClick;
|
||||
const publisherNode =
|
||||
!isBuiltin && agent.owner_username ? (
|
||||
<div
|
||||
className="text-muted-foreground flex items-center gap-1 text-xs"
|
||||
title={`发布者:${desensitize(agent.owner_username)}`}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canFilterByPublisher}
|
||||
onClick={(e) => {
|
||||
if (!canFilterByPublisher) return;
|
||||
e.stopPropagation();
|
||||
onPublisherClick!({ id: agent.user_id!, label: publisherLabel });
|
||||
}}
|
||||
className={cn(
|
||||
"text-muted-foreground flex max-w-full items-center gap-1 text-sm font-medium",
|
||||
canFilterByPublisher &&
|
||||
"hover:text-primary cursor-pointer transition-colors hover:underline",
|
||||
)}
|
||||
title={
|
||||
canFilterByPublisher
|
||||
? `点击筛选发布者:${publisherLabel}`
|
||||
: `发布者:${publisherLabel}`
|
||||
}
|
||||
>
|
||||
<UserIcon className="size-3 shrink-0" />
|
||||
<span className="truncate">发布者:{desensitize(agent.owner_username)}</span>
|
||||
</div>
|
||||
<UserIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">发布者:{publisherLabel}</span>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
// 技能英文名 → 中文名映射,用于把智能体的技能徽章显示为中文(agent.skills 只存英文名)。
|
||||
@@ -816,6 +859,17 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
已选 {selectedSkills.length} 个
|
||||
</span>
|
||||
</div>
|
||||
{enabledSkills.length > 0 && (
|
||||
<div className="relative">
|
||||
<SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder="搜索技能(支持中英文名称、描述)…"
|
||||
value={skillSearch}
|
||||
onChange={(e) => setSkillSearch(e.currentTarget.value)}
|
||||
className="h-8 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[240px] overflow-y-auto rounded-md border p-2">
|
||||
{isSkillsLoading ? (
|
||||
<p className="text-muted-foreground text-sm">技能加载中...</p>
|
||||
@@ -823,9 +877,11 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可用技能
|
||||
</p>
|
||||
) : filteredSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">没有匹配的技能</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{enabledSkills.map((skill) => {
|
||||
{filteredSkills.map((skill) => {
|
||||
const checked = selectedSkills.includes(skill.name);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownUpIcon, BotIcon, LockIcon, PlusIcon, ShieldIcon } from "lucide-react";
|
||||
import { ArrowDownUpIcon, BotIcon, LockIcon, PlusIcon, ShieldIcon, XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -36,6 +36,11 @@ export function AgentGallery() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [tagIds, setTagIds] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<AgentSort>("time_desc");
|
||||
// Publisher filter: click a card's 发布者 to keep only that user's agents.
|
||||
// {id} is the owner user_id (server filter), {label} the desensitized name.
|
||||
const [publisher, setPublisher] = useState<{ id: string; label: string } | null>(
|
||||
null,
|
||||
);
|
||||
// Per-tab 1-based page index, preserved while switching tabs.
|
||||
const [pages, setPages] = useState<{ all: number; mine: number; private: number }>(
|
||||
{ all: 1, mine: 1, private: 1 },
|
||||
@@ -48,7 +53,7 @@ export function AgentGallery() {
|
||||
const tagKey = tagIds.join(",");
|
||||
useEffect(() => {
|
||||
setPages({ all: 1, mine: 1, private: 1 });
|
||||
}, [search, tagKey, sort]);
|
||||
}, [search, tagKey, sort, publisher?.id]);
|
||||
|
||||
const { data: privateStatus } = usePrivateSquareStatus();
|
||||
const privateFeatureAvailable = !!privateStatus?.enabled;
|
||||
@@ -77,6 +82,7 @@ export function AgentGallery() {
|
||||
page: mainPage,
|
||||
pageSize: PAGE_SIZE,
|
||||
enabled: activeTab !== "private",
|
||||
owner: publisher?.id,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -91,6 +97,7 @@ export function AgentGallery() {
|
||||
page: pages.private,
|
||||
pageSize: PAGE_SIZE,
|
||||
enabled: activeTab === "private",
|
||||
owner: publisher?.id,
|
||||
});
|
||||
|
||||
// If the server reports the stored password no longer works (likely the
|
||||
@@ -127,17 +134,29 @@ export function AgentGallery() {
|
||||
setPrivatePassword(null);
|
||||
};
|
||||
|
||||
const handlePublisherClick = (p: { id: string; label: string }) =>
|
||||
setPublisher(p);
|
||||
|
||||
const renderGrid = (list: Agent[]) =>
|
||||
viewMode === "list" ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
{list.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} layout="list" />
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
layout="list"
|
||||
onPublisherClick={handlePublisherClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{list.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onPublisherClick={handlePublisherClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -356,6 +375,20 @@ export function AgentGallery() {
|
||||
</Select>
|
||||
}
|
||||
/>
|
||||
{publisher && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">筛选发布者:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPublisher(null)}
|
||||
className="bg-primary/10 text-primary hover:bg-primary/20 inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors"
|
||||
title="点击清除发布者筛选"
|
||||
>
|
||||
{publisher.label}
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TabsContent value="all" className="flex-1 overflow-y-auto p-6 mt-0">
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TrendingUpIcon,
|
||||
Upload,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -154,6 +155,11 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
const [tagIds, setTagIds] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<SkillSort>("time_desc");
|
||||
const [page, setPage] = useState(1);
|
||||
// Publisher filter: click a skill's 发布者 to keep only that user's skills.
|
||||
// {id} is owner_user_id; {label} the desensitized display name.
|
||||
const [publisher, setPublisher] = useState<{ id: string; label: string } | null>(
|
||||
null,
|
||||
);
|
||||
const [viewMode, setViewMode] = useViewMode("skill");
|
||||
const reconcileMutation = useReconcileCustomSkills();
|
||||
const fillNamesMutation = useGenerateMissingSkillNameZh();
|
||||
@@ -203,7 +209,14 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
};
|
||||
}, [skills, currentUserId]);
|
||||
|
||||
const filteredSkills = filter === "mine" ? activeMine : activeAll;
|
||||
const scopedSkills = filter === "mine" ? activeMine : activeAll;
|
||||
const filteredSkills = useMemo(
|
||||
() =>
|
||||
publisher
|
||||
? scopedSkills.filter((s) => s.owner_user_id === publisher.id)
|
||||
: scopedSkills,
|
||||
[scopedSkills, publisher],
|
||||
);
|
||||
const orderedSkills = useMemo(
|
||||
() =>
|
||||
filteredSkills
|
||||
@@ -228,7 +241,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
}, [page, totalPages]);
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [search, tagIds, sort]);
|
||||
}, [search, tagIds, sort, publisher?.id, filter]);
|
||||
const pagedSkills = useMemo(
|
||||
() => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||
[orderedSkills, page],
|
||||
@@ -344,12 +357,13 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="gap-1.5 border-violet-300 bg-violet-50 font-medium text-violet-700 hover:bg-violet-100 hover:text-violet-800 dark:border-violet-500/40 dark:bg-violet-500/15 dark:text-violet-300 dark:hover:bg-violet-500/25"
|
||||
onClick={() => void handleFillNames()}
|
||||
disabled={
|
||||
fillNamesMutation.isPending ||
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
|
||||
}
|
||||
title="为所有尚未配置中文名称的技能自动生成中文名"
|
||||
title="用大模型为所有尚未配置中文名称的技能自动生成中文名"
|
||||
>
|
||||
{fillNamesMutation.isPending ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
@@ -399,6 +413,20 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{publisher && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">筛选发布者:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPublisher(null)}
|
||||
className="bg-primary/10 text-primary hover:bg-primary/20 inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors"
|
||||
title="点击清除发布者筛选"
|
||||
>
|
||||
{publisher.label}
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
|
||||
)}
|
||||
@@ -421,6 +449,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||
currentUserId={currentUserId}
|
||||
isAdmin={isAdmin}
|
||||
layout={viewMode === "card" ? "card" : "row"}
|
||||
onPublisherClick={setPublisher}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -478,12 +507,16 @@ export function SkillRow({
|
||||
currentUserId,
|
||||
isAdmin,
|
||||
layout = "row",
|
||||
onPublisherClick,
|
||||
}: {
|
||||
skill: Skill;
|
||||
currentUserId: string | null;
|
||||
isAdmin: boolean;
|
||||
/** "row" = full-width horizontal item (default); "card" = grid card cell. */
|
||||
layout?: "row" | "card";
|
||||
/** Click the 发布者 line to filter the list to that publisher. Receives the
|
||||
* owner_user_id + its desensitized display label. */
|
||||
onPublisherClick?: (publisher: { id: string; label: string }) => void;
|
||||
}) {
|
||||
const isCard = layout === "card";
|
||||
const { t } = useI18n();
|
||||
@@ -674,10 +707,31 @@ export function SkillRow({
|
||||
const metaNodes = (
|
||||
<>
|
||||
{!isBuiltin && skill.owner_username && (
|
||||
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
|
||||
<UserIcon className="size-3" />
|
||||
发布者:{desensitize(skill.owner_username)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!skill.owner_user_id || !onPublisherClick}
|
||||
onClick={(e) => {
|
||||
if (!skill.owner_user_id || !onPublisherClick) return;
|
||||
e.stopPropagation();
|
||||
onPublisherClick({
|
||||
id: skill.owner_user_id,
|
||||
label: desensitize(skill.owner_username!),
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"text-muted-foreground mt-1 flex max-w-full items-center gap-1 text-sm font-medium",
|
||||
skill.owner_user_id && onPublisherClick &&
|
||||
"hover:text-primary cursor-pointer transition-colors hover:underline",
|
||||
)}
|
||||
title={
|
||||
skill.owner_user_id && onPublisherClick
|
||||
? `点击筛选发布者:${desensitize(skill.owner_username)}`
|
||||
: `发布者:${desensitize(skill.owner_username)}`
|
||||
}
|
||||
>
|
||||
<UserIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">发布者:{desensitize(skill.owner_username)}</span>
|
||||
</button>
|
||||
)}
|
||||
{skill.use_count !== undefined && (
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PencilIcon,
|
||||
SlidersHorizontalIcon,
|
||||
SparklesIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -106,6 +107,7 @@ export function SkillDetailDialog({
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [editingNameZh, setEditingNameZh] = useState(false);
|
||||
const [nameZhDraft, setNameZhDraft] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"detail" | "display" | "source" | "files" | null
|
||||
@@ -137,6 +139,7 @@ export function SkillDetailDialog({
|
||||
|
||||
useEffect(() => {
|
||||
setEditing(false);
|
||||
setEditingNameZh(false);
|
||||
setActiveTab(null);
|
||||
setShowRawSource(false);
|
||||
setOpenedFilePath(null);
|
||||
@@ -169,6 +172,11 @@ export function SkillDetailDialog({
|
||||
}
|
||||
}
|
||||
|
||||
function startEditingNameZh() {
|
||||
setNameZhDraft(skill?.name_zh ?? "");
|
||||
setEditingNameZh(true);
|
||||
}
|
||||
|
||||
async function handleSaveNameZh() {
|
||||
if (!skill) return;
|
||||
try {
|
||||
@@ -177,6 +185,7 @@ export function SkillDetailDialog({
|
||||
nameZh: nameZhDraft.trim(),
|
||||
});
|
||||
toast.success("中文名称已保存");
|
||||
setEditingNameZh(false);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
@@ -215,32 +224,99 @@ export function SkillDetailDialog({
|
||||
<SparklesIcon className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DialogTitle className="truncate text-base">
|
||||
{skill?.name_zh?.trim()
|
||||
? skill.name_zh
|
||||
: desensitize(skill?.name ?? skillName ?? "")}
|
||||
</DialogTitle>
|
||||
{skill?.name_zh?.trim() && (
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{editingNameZh ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DialogTitle className="sr-only">
|
||||
{desensitize(skill?.name ?? skillName ?? "")}
|
||||
</span>
|
||||
)}
|
||||
{skill?.builtin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{tx.badgeBuiltin}
|
||||
</Badge>
|
||||
)}
|
||||
{skill?.published && (
|
||||
<Badge
|
||||
</DialogTitle>
|
||||
<Input
|
||||
value={nameZhDraft}
|
||||
onChange={(event) => setNameZhDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") void handleSaveNameZh();
|
||||
if (event.key === "Escape") setEditingNameZh(false);
|
||||
}}
|
||||
placeholder={desensitize(skill?.name ?? skillName ?? "")}
|
||||
className="h-8 max-w-[18rem] flex-1 text-sm"
|
||||
autoFocus
|
||||
disabled={generateNameZh.isPending || setNameZh.isPending}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-sky-500/30 text-xs text-sky-700 dark:text-sky-300"
|
||||
className="h-8 shrink-0 gap-1 border-violet-300 bg-violet-50 px-2.5 text-xs font-medium text-violet-700 hover:bg-violet-100 hover:text-violet-800 dark:border-violet-500/40 dark:bg-violet-500/15 dark:text-violet-300 dark:hover:bg-violet-500/25"
|
||||
onClick={() => void handleGenerateNameZh()}
|
||||
disabled={generateNameZh.isPending || setNameZh.isPending}
|
||||
title="用大模型自动生成中文名"
|
||||
>
|
||||
<GlobeIcon className="size-3" />
|
||||
{tx.badgePublished}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{generateNameZh.isPending ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<SparklesIcon className="size-3.5" />
|
||||
)}
|
||||
AI 生成
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 shrink-0 px-3 text-xs"
|
||||
onClick={() => void handleSaveNameZh()}
|
||||
disabled={setNameZh.isPending || generateNameZh.isPending}
|
||||
>
|
||||
{setNameZh.isPending && (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
)}
|
||||
{t.common.save}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground size-8 shrink-0"
|
||||
onClick={() => setEditingNameZh(false)}
|
||||
disabled={setNameZh.isPending}
|
||||
title={t.common.cancel}
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="group flex flex-wrap items-center gap-2">
|
||||
<DialogTitle className="truncate text-base">
|
||||
{skill?.name_zh?.trim()
|
||||
? skill.name_zh
|
||||
: desensitize(skill?.name ?? skillName ?? "")}
|
||||
</DialogTitle>
|
||||
{skill?.name_zh?.trim() && (
|
||||
<span className="text-muted-foreground truncate font-mono text-xs">
|
||||
{desensitize(skill?.name ?? skillName ?? "")}
|
||||
</span>
|
||||
)}
|
||||
{canManage && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground size-6 shrink-0 opacity-60 transition-opacity hover:opacity-100"
|
||||
onClick={startEditingNameZh}
|
||||
title={skill?.name_zh?.trim() ? "编辑中文名" : "设置中文名"}
|
||||
>
|
||||
<PencilIcon className="size-3" />
|
||||
</Button>
|
||||
)}
|
||||
{skill?.builtin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{tx.badgeBuiltin}
|
||||
</Badge>
|
||||
)}
|
||||
{skill?.published && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-sky-500/30 text-xs text-sky-700 dark:text-sky-300"
|
||||
>
|
||||
<GlobeIcon className="size-3" />
|
||||
{tx.badgePublished}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogDescription className="mt-1 line-clamp-2 text-sm leading-relaxed">
|
||||
{skill?.description ? desensitize(skill.description) : "-"}
|
||||
</DialogDescription>
|
||||
@@ -336,57 +412,6 @@ export function SkillDetailDialog({
|
||||
className="m-0 min-h-0 flex-1 overscroll-contain px-6 py-5 data-[state=inactive]:hidden"
|
||||
style={{ overflowY: "auto" }}
|
||||
>
|
||||
<div className="mb-5 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">中文名称</span>
|
||||
{canManage && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() => void handleGenerateNameZh()}
|
||||
disabled={generateNameZh.isPending || setNameZh.isPending}
|
||||
>
|
||||
{generateNameZh.isPending ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<SparklesIcon className="size-3.5" />
|
||||
)}
|
||||
AI 生成
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{canManage ? (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Input
|
||||
value={nameZhDraft}
|
||||
onChange={(event) => setNameZhDraft(event.currentTarget.value)}
|
||||
placeholder="为该技能设置中文名称(留空则使用英文名)"
|
||||
className="h-8 text-sm"
|
||||
disabled={generateNameZh.isPending}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => void handleSaveNameZh()}
|
||||
disabled={
|
||||
setNameZh.isPending ||
|
||||
generateNameZh.isPending ||
|
||||
nameZhDraft.trim() === (skill?.name_zh ?? "").trim()
|
||||
}
|
||||
>
|
||||
{setNameZh.isPending && (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
)}
|
||||
{t.common.save}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
{skill?.name_zh?.trim() ? skill.name_zh : "未设置"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{editing ? (
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
|
||||
Reference in new issue
Block a user