"use client"; import { CopyIcon, EyeIcon, FileCode2Icon, FilesIcon, FileTextIcon, GlobeIcon, Loader2, PencilIcon, SlidersHorizontalIcon, SparklesIcon, XIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { MarkdownContent } from "@/components/workspace/messages/markdown-content"; import { ModelNameSelect } from "@/components/workspace/model-name-select"; import { SkillDisplayAdapterPanel } from "@/components/workspace/skill-display-adapter-panel"; import { SkillFilePreviewDialog } from "@/components/workspace/skills/skill-file-preview-dialog"; import { SkillFileTree } from "@/components/workspace/skills/skill-file-tree"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import { useGenerateSkillNameZh, useSetSkillAlwaysOn, useSetSkillDetail, useSetSkillNameZh, useSkill, useSkillContent, useSkillFiles, } from "@/core/skills/hooks"; import { useSkillCompressionSettings } from "@/core/system-settings/hooks"; import type { Skill } from "@/core/skills/type"; import { reasoningPlugins } from "@/core/streamdown"; import { desensitize } from "@/lib/desensitize"; const markdownClassName = "!h-auto !w-full max-w-none min-w-0 text-sm leading-7 " + "[&_h1]:mt-0 [&_h1]:mb-3 [&_h1]:text-xl [&_h1]:font-semibold " + "[&_h2]:mt-6 [&_h2]:mb-2 [&_h2]:border-b [&_h2]:pb-2 [&_h2]:text-lg [&_h2]:font-semibold " + "[&_h3]:mt-5 [&_h3]:mb-2 [&_h3]:text-base [&_h3]:font-semibold " + "[&_p]:my-3 [&_ul]:my-3 [&_ol]:my-3 [&_li]:my-1.5 " + "[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 " + "[&_blockquote]:my-4 [&_blockquote]:border-l-4 [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground " + "[&_code]:rounded [&_code]:border [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs " + "[&_pre]:my-4 [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:bg-muted [&_pre]:p-4 " + "[&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0 " + "[&_table]:my-4 [&_table]:w-full [&_table]:min-w-max [&_table]:border-collapse " + "[&_th]:border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_td]:border [&_td]:px-3 [&_td]:py-2"; function canManageSkill(skill: Skill | null | undefined): boolean { if (!skill) return false; const auth = getStoredAuth(); if (!auth) return false; if (auth.system_role === "admin") return true; return ( skill.category === "custom" && !!skill.owner_user_id && skill.owner_user_id === auth.user_id ); } function isSkillOwner(skill: Skill | null | undefined): boolean { if (!skill) return false; const auth = getStoredAuth(); if (!auth) return false; return ( skill.category === "custom" && !skill.builtin && !!skill.owner_user_id && skill.owner_user_id === auth.user_id ); } function stripYamlFrontmatter(content: string): string { return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trimStart(); } export function SkillDetailDialog({ skillName, open, onOpenChange, }: { skillName: string | null; open: boolean; onOpenChange: (open: boolean) => void; }) { const { t } = useI18n(); const tx = t.settings.skills; const activeName = open ? skillName : null; const { data: skill, isLoading: isSkillLoading } = useSkill(activeName); const setDetail = useSetSkillDetail(); const setNameZh = useSetSkillNameZh(); const generateNameZh = useGenerateSkillNameZh(); const setAlwaysOn = useSetSkillAlwaysOn(); const { data: compression } = useSkillCompressionSettings(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); const [editingNameZh, setEditingNameZh] = useState(false); const [nameZhDraft, setNameZhDraft] = useState(""); // Optional model override for the "AI 生成" button (null = configured default). const [nameZhModel, setNameZhModel] = useState(null); const [activeTab, setActiveTab] = useState< "detail" | "display" | "source" | "files" | null >(null); const [showRawSource, setShowRawSource] = useState(false); const [openedFilePath, setOpenedFilePath] = useState(null); const canManage = useMemo(() => canManageSkill(skill), [skill]); const isOwner = useMemo(() => isSkillOwner(skill), [skill]); // Admins may browse any skill's files (the backend authorizes this); owners // may browse their own custom skill. Edit-ability is decided separately by // the backend's `filesData.editable` flag. const canViewFiles = useMemo(() => { if (!skill) return false; return isOwner || getStoredAuth()?.system_role === "admin"; }, [skill, isOwner]); const { data: contentData, isLoading: isContentLoading, error: contentError, } = useSkillContent(open ? skillName : null); const filesEnabled = open && canViewFiles && activeTab === "files"; const { data: filesData, isLoading: isFilesLoading, error: filesError, } = useSkillFiles(filesEnabled ? skillName : null); useEffect(() => { setEditing(false); setEditingNameZh(false); setActiveTab(null); setShowRawSource(false); setOpenedFilePath(null); }, [open, skillName]); useEffect(() => { setNameZhDraft(skill?.name_zh ?? ""); }, [skill?.name, skill?.name_zh]); const skillBody = useMemo(() => { return stripYamlFrontmatter(contentData?.content ?? ""); }, [contentData]); const defaultTab: "detail" = "detail"; const selectedTab = activeTab ?? defaultTab; function startEditing() { setDraft(skill?.detail ?? ""); setEditing(true); } async function handleSave() { if (!skill) return; try { await setDetail.mutateAsync({ skillName: skill.name, detail: draft }); toast.success(tx.detailSaveSuccess); setEditing(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } function startEditingNameZh() { setNameZhDraft(skill?.name_zh ?? ""); setEditingNameZh(true); } async function handleSaveNameZh() { if (!skill) return; try { await setNameZh.mutateAsync({ skillName: skill.name, nameZh: nameZhDraft.trim(), }); toast.success("中文名称已保存"); setEditingNameZh(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } async function handleGenerateNameZh() { if (!skill) return; try { const result = await generateNameZh.mutateAsync({ skillName: skill.name, modelName: nameZhModel, }); 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"); } return ( <>
{editingNameZh ? (
{desensitize(skill?.name ?? skillName ?? "")} 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-[14rem] flex-1 text-sm" autoFocus disabled={generateNameZh.isPending || setNameZh.isPending} />
) : (
{skill?.name_zh?.trim() ? skill.name_zh : desensitize(skill?.name ?? skillName ?? "")} {skill?.name_zh?.trim() && ( {desensitize(skill?.name ?? skillName ?? "")} )} {canManage && ( )} {skill?.builtin && ( {tx.badgeBuiltin} )} {skill?.published && ( {tx.badgePublished} )} {skill?.always_on && compression?.enabled && ( 常驻 )}
)} {skill?.description ? desensitize(skill.description) : "-"}
{isSkillLoading || !skill ? (
{t.common.loading}
) : ( setActiveTab(value as Exclude) } className="flex min-h-0 flex-1 flex-col gap-0" >
{tx.detailSectionTitle} {canManage && ( 展示适配 )} {canViewFiles && ( {tx.filesSectionTitle} )}
{canManage && compression?.enabled && skill && (
常驻(免压缩) setAlwaysOn.mutate( { skillName: skill.name, alwaysOn: next }, { onSuccess: () => toast.success(next ? "已设为常驻(免压缩)" : "已取消常驻"), onError: (e) => toast.error( e instanceof Error ? e.message : "更新常驻状态失败", ), }, ) } />
)} {selectedTab === "detail" && canManage && !editing && ( )} {selectedTab === "source" && !isContentLoading && !contentError && ( <> )}
{editing ? (