feat(skills): 点击技能展示 SKILL.md 详情弹窗

- 后端新增 GET /api/skills/{name}/content 接口,返回公共/自定义技能的 SKILL.md 原文
- 前端技能行内容区可点击,弹窗以等宽原文展示 SKILL.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Opus 4.7 committed 2026-05-15 15:23:34 +08:00
1 parent 12798c00bc
commit 6981fc06ca
4 files changed
+108 -1

No files matched your search

@@ -54,6 +54,7 @@ import {
useInstallSkillUpload,
useSetSkillPinned,
useSetSkillPublished,
useSkillContent,
useSkills,
useValidateSkillUpload,
} from "@/core/skills/hooks";
@@ -256,6 +257,7 @@ function SkillRow({
const setPinned = useSetSkillPinned();
const deleteSkill = useDeleteCustomSkill();
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const isCustom = skill.category === "custom";
// Built-in flag falls back to "no DB owner" so legacy responses without
@@ -306,7 +308,10 @@ function SkillRow({
return (
<>
<Item className="w-full" variant="outline">
<ItemContent>
<ItemContent
className="cursor-pointer"
onClick={() => setDetailOpen(true)}
>
<ItemTitle>
<div className="flex flex-wrap items-center gap-2">
<span>{skill.name}</span>
@@ -471,10 +476,55 @@ function SkillRow({
</DialogFooter>
</DialogContent>
</Dialog>
<SkillDetailDialog
skill={skill}
open={detailOpen}
onOpenChange={setDetailOpen}
/>
</>
);
}
function SkillDetailDialog({
skill,
open,
onOpenChange,
}: {
skill: Skill;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useI18n();
const { data, isLoading, error } = useSkillContent(open ? skill.name : null);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[80vh] flex-col gap-3 sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{skill.name}</DialogTitle>
<DialogDescription>{skill.description}</DialogDescription>
</DialogHeader>
<div className="bg-muted/40 min-h-0 flex-1 overflow-auto rounded-md border">
{isLoading ? (
<div className="text-muted-foreground flex items-center justify-center gap-2 py-16 text-sm">
<Loader2 className="size-4 animate-spin" />
{t.common.loading}
</div>
) : error ? (
<div className="text-destructive p-4 text-sm">
{error instanceof Error ? error.message : String(error)}
</div>
) : (
<pre className="text-foreground/90 whitespace-pre-wrap wrap-break-word p-4 font-mono text-xs leading-relaxed">
{data?.content}
</pre>
)}
</div>
</DialogContent>
</Dialog>
);
}
function EmptySkill({ onCreateSkill }: { onCreateSkill: () => void }) {
const { t } = useI18n();
return (