style(knowledge): 实体改回纯展示(不可点击),chip 精致化

按需求去掉点击筛选,实体为静态彩色 chip:色点 + 名称 + 胶囊型类型标签。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdministratorandClaude Opus 4.8 committed 2026-06-09 20:34:59 +08:00
1 parent f6af9379b7
commit 1ce15fbb15
15 files changed
+596 -38

No files matched your search

@@ -510,6 +510,15 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
</div>
) : null;
// 技能英文名 → 中文名映射,用于把智能体的技能徽章显示为中文(agent.skills 只存英文名)。
const skillNameZhMap = useMemo(() => {
const map = new Map<string, string>();
for (const s of skills) {
if (s.name_zh?.trim()) map.set(s.name, s.name_zh.trim());
}
return map;
}, [skills]);
const skillBadgeItems = [
...(agent.tool_groups ?? []).map((group) => ({
key: `tg:${group}`,
@@ -532,7 +541,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
setDetailSkillName(skill);
}}
>
{desensitize(skill)}
{skillNameZhMap.get(skill) ?? desensitize(skill)}
</Badge>
),
})),
@@ -831,7 +840,11 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
title={desensitize(skill.description ?? "")}
className="h-8 min-w-0 flex-1 rounded-r-none"
>
<span className="truncate">{desensitize(skill.name)}</span>
<span className="truncate">
{skill.name_zh?.trim()
? skill.name_zh
: desensitize(skill.name)}
</span>
</Button>
<Button
type="button"
@@ -68,6 +68,7 @@ import type { ValidateSkillUploadResponse } from "@/core/skills/api";
import {
useDeleteCustomSkill,
useDuplicateSkill,
useGenerateMissingSkillNameZh,
useInstallSkillUpload,
useReconcileCustomSkills,
useSetSkillFeatured,
@@ -155,6 +156,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
const [page, setPage] = useState(1);
const [viewMode, setViewMode] = useViewMode("skill");
const reconcileMutation = useReconcileCustomSkills();
const fillNamesMutation = useGenerateMissingSkillNameZh();
// Name + tag filtering is resolved server-side via the /api/skills query.
const { skills, isLoading, error } = useSkills({ search, tagIds, sort });
@@ -232,6 +234,23 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
[orderedSkills, page],
);
const handleFillNames = async () => {
try {
const result = await fillNamesMutation.mutateAsync();
const okCount = result.generated.length;
const failCount = result.failed.length;
if (okCount === 0 && failCount === 0) {
toast.info("所有技能均已配置中文名称");
} else if (failCount === 0) {
toast.success(`已为 ${okCount} 个技能补充中文名称`);
} else {
toast.warning(`已补充 ${okCount} 个,${failCount} 个生成失败,可稍后重试或手动填写`);
}
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
};
const handleRescan = async () => {
try {
const result = await reconcileMutation.mutateAsync();
@@ -322,6 +341,23 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
? t.settings.skills.rescanSkillsRunning
: t.settings.skills.rescanSkills}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => void handleFillNames()}
disabled={
fillNamesMutation.isPending ||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
}
title="为所有尚未配置中文名称的技能自动生成中文名"
>
{fillNamesMutation.isPending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<SparklesIcon className="size-4" />
)}
{fillNamesMutation.isPending ? "补充中…" : "一键补充中文名"}
</Button>
<Button
size="sm"
variant="outline"
@@ -550,8 +586,13 @@ export function SkillRow({
const badgeGroup = (
<div className="flex flex-wrap items-center gap-2">
<span className={cn(isCard && "truncate font-medium")}>
{desensitize(skill.name)}
{skill.name_zh?.trim() ? skill.name_zh : desensitize(skill.name)}
</span>
{skill.name_zh?.trim() && (
<span className="text-muted-foreground truncate text-xs font-normal">
{desensitize(skill.name)}
</span>
)}
{isBuiltin && (
<Badge variant="secondary" className="text-xs">
{t.settings.skills.badgeBuiltin}
@@ -24,6 +24,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
@@ -33,7 +34,9 @@ import { SkillFileTree } from "@/components/workspace/skills/skill-file-tree";
import { getStoredAuth } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
import {
useGenerateSkillNameZh,
useSetSkillDetail,
useSetSkillNameZh,
useSkill,
useSkillContent,
useSkillFiles,
@@ -98,9 +101,12 @@ export function SkillDetailDialog({
const activeName = open ? skillName : null;
const { data: skill, isLoading: isSkillLoading } = useSkill(activeName);
const setDetail = useSetSkillDetail();
const setNameZh = useSetSkillNameZh();
const generateNameZh = useGenerateSkillNameZh();
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState("");
const [nameZhDraft, setNameZhDraft] = useState("");
const [activeTab, setActiveTab] = useState<
"detail" | "display" | "source" | "files" | null
>(null);
@@ -136,6 +142,10 @@ export function SkillDetailDialog({
setOpenedFilePath(null);
}, [open, skillName]);
useEffect(() => {
setNameZhDraft(skill?.name_zh ?? "");
}, [skill?.name, skill?.name_zh]);
const skillBody = useMemo(() => {
return stripYamlFrontmatter(contentData?.content ?? "");
}, [contentData]);
@@ -159,6 +169,30 @@ export function SkillDetailDialog({
}
}
async function handleSaveNameZh() {
if (!skill) return;
try {
await setNameZh.mutateAsync({
skillName: skill.name,
nameZh: nameZhDraft.trim(),
});
toast.success("中文名称已保存");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleGenerateNameZh() {
if (!skill) return;
try {
const result = await generateNameZh.mutateAsync(skill.name);
setNameZhDraft(result.name_zh ?? "");
toast.success("已生成中文名称");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function copySource() {
await navigator.clipboard.writeText(contentData?.content ?? "");
toast.success("Copied SKILL.md content");
@@ -183,8 +217,15 @@ export function SkillDetailDialog({
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<DialogTitle className="truncate text-base">
{desensitize(skill?.name ?? skillName ?? "")}
{skill?.name_zh?.trim()
? skill.name_zh
: desensitize(skill?.name ?? skillName ?? "")}
</DialogTitle>
{skill?.name_zh?.trim() && (
<span className="text-muted-foreground truncate text-xs">
{desensitize(skill?.name ?? skillName ?? "")}
</span>
)}
{skill?.builtin && (
<Badge variant="secondary" className="text-xs">
{tx.badgeBuiltin}
@@ -295,6 +336,57 @@ 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