"use client"; import { AlertCircle, Anchor, ArchiveIcon, ArrowDownUpIcon, BookmarkIcon, CheckCircle2, ChevronLeftIcon, ChevronRightIcon, ClockIcon, CopyIcon, EyeOffIcon, FileArchive, GlobeIcon, InfoIcon, Loader2, LockIcon, PinIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TagsIcon, Trash2Icon, TrendingUpIcon, Upload, UserIcon, XIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } 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 { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; import { Item, ItemActions, ItemTitle, ItemContent, ItemDescription, } from "@/components/ui/item"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ModelNameSelect } from "@/components/workspace/model-name-select"; import { PublishSquarePickerDialog } from "@/components/workspace/publish-square-picker-dialog"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar"; import { ViewModeToggle } from "@/components/workspace/view-mode-toggle"; import { useTheme } from "@/contexts/ThemeContext"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import type { ValidateSkillUploadResponse } from "@/core/skills/api"; import { useDeleteBuiltinSkill, useDeleteCustomSkill, useDuplicateSkill, useGenerateMissingSkillNameZh, useGenerateSkillNameZh, useInstallSkillUpload, useReconcileCustomSkills, useSetSkillAlwaysOn, useSetSkillFeatured, useSetSkillNameZh, useSetSkillPinned, useSetSkillPublished, useSkills, useValidateSkillUpload, } from "@/core/skills/hooks"; import { usePublishSquares, useSkillCompressionSettings, useViewMode, } from "@/core/system-settings/hooks"; import type { Skill } from "@/core/skills/type"; import type { SkillKnowledgeBinding } from "@/strategy-components/api/skill-knowledge"; import { formatDateTime } from "@/core/utils/datetime"; import { env } from "@/env"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; import { SettingsSection } from "./settings-section"; import { SkillStateSheet } from "./skill-state-sheet"; export function SkillSettingsPage({ onClose, embedded = false, }: { onClose?: () => void; embedded?: boolean; } = {}) { const { t } = useI18n(); if (embedded) return ; return ( ); } const PAGE_SIZE = 10; type SkillSort = | "time_desc" | "time_asc" | "activity_desc" | "activity_asc" | "usage_desc" | "usage_asc" | "name_asc" | "name_desc"; /** 重新扫描成功 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */ const rescanSuccessToastDarkOptions = { classNames: { toast: "!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md", title: "!text-[#FFFFFF]", description: "!text-[#FFFFFF]", }, icon: , } as const; /** 重新扫描无变更 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */ const rescanInfoToastDarkOptions = { classNames: { toast: "!rounded-[2px] !border !border-[#704812] !bg-[#202222] !text-[#FFFFFF] shadow-md", title: "!text-[#FFFFFF]", description: "!text-[#FFFFFF]", }, icon: , } as const; /** 重新扫描失败 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */ const rescanErrorToastDarkOptions = { classNames: { toast: "!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md", title: "!text-[#FFFFFF]", description: "!text-[#FFFFFF]", }, icon: , } as const; function SkillSettingsList({ onClose }: { onClose?: () => void }) { const { t } = useI18n(); const { theme } = useTheme(); const router = useRouter(); const [filter, setFilter] = useState("mine"); const [installOpen, setInstallOpen] = useState(false); const [staleSheetOpen, setStaleSheetOpen] = useState(false); const [archiveSheetOpen, setArchiveSheetOpen] = useState(false); const [search, setSearch] = useState(""); const [tagIds, setTagIds] = useState([]); const [sort, setSort] = useState("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 { data: publishSquares } = usePublishSquares(); // Each configured publish square (发布广场) becomes its own browse tab; the // backend always returns at least one (the built-in default). const squareTabs = publishSquares?.squares ?? []; const reconcileMutation = useReconcileCustomSkills(); const fillNamesMutation = useGenerateMissingSkillNameZh(); // Optional model override for the admin "一键补充中文名" action (null = default). const [fillModel, setFillModel] = useState(null); const storedAuth = useMemo(() => getStoredAuth(), []); const currentUserId = storedAuth?.user_id ?? null; const isAdmin = storedAuth?.system_role === "admin"; // Name + tag filtering is resolved server-side via the /api/skills query. // A 岗位 (position) no longer *restricts* the list — it grants skills into the // member's 我的技能 (each such skill comes back with ``favorited: true``), so // the 广场 always shows every visible skill and the 我的 tab includes the // position-granted ones. ``scope`` is kept for API compatibility only. const { skills, isLoading, error } = useSkills({ search, tagIds, sort, scope: isAdmin ? undefined : "mine", }); // 把可见技能分类成 UI 关心的几个桶。 // "All" = 内置 + 已发布的技能(仅 active 状态);自己的技能也要自己发布了才出现。 // "Mine" = 严格属于我自己的自定义技能(仅 active 状态)。 const { activeAll, activeMine, staleVisible, archivedVisible } = useMemo(() => { const all: Skill[] = []; const mine: Skill[] = []; const stale: Skill[] = []; const archived: Skill[] = []; for (const skill of skills) { const state = skill.state ?? "active"; const isCustom = skill.category === "custom"; const isOwnedByMe = isCustom && !!skill.owner_user_id && skill.owner_user_id === currentUserId; const isBuiltin = skill.builtin ?? !skill.owner_user_id; if (state === "stale") { stale.push(skill); continue; } if (state === "archived") { archived.push(skill); continue; } // state === active if (isBuiltin || !!skill.published) { all.push(skill); } // "我的" = skills I own OR skills granted into my collection by my 岗位 // (position) — mirroring how favorited agents land in 我的智能体. if (isOwnedByMe || !!skill.favorited) { mine.push(skill); } } return { activeAll: all, activeMine: mine, staleVisible: stale, archivedVisible: archived, }; }, [skills, currentUserId]); // "mine" tab shows my own skills; every other tab value is a square id whose // public skills come from the published pool. const isSquareTab = filter !== "mine"; const scopedSkills = filter === "mine" ? activeMine : activeAll; const filteredSkills = useMemo(() => { let list = publisher ? scopedSkills.filter((s) => s.owner_user_id === publisher.id) : scopedSkills; // On a square tab, keep only that square's skills. The default square also // sweeps in legacy/unassigned published skills (square_id === ""). if (isSquareTab) { const isDefault = filter === publishSquares?.default_square_id; list = list.filter((s) => { const sid = s.square_id ?? ""; return sid === filter || (isDefault && !sid); }); } return list; }, [scopedSkills, publisher, filter, isSquareTab, publishSquares?.default_square_id]); const orderedSkills = useMemo( () => filteredSkills .map((skill, index) => ({ skill, index })) .sort((a, b) => { // 置顶(featured)决定排序;钉选(pinned)只管防归档,不影响顺序。 const featuredDelta = Number(!!b.skill.featured) - Number(!!a.skill.featured); return featuredDelta || a.index - b.index; }) .map(({ skill }) => skill), [filteredSkills], ); const staleCount = staleVisible.length; const archivedCount = archivedVisible.length; const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE)); // Filter / search / tag changes may shrink the result set — pull the page // back into range without forcing a reset on every keystroke. useEffect(() => { if (page > totalPages) setPage(totalPages); }, [page, totalPages]); useEffect(() => { setPage(1); }, [search, tagIds, sort, publisher?.id, filter]); // If the active square tab disappears (admin removed that square), fall back // to "我的" so the page never points at a dead tab. useEffect(() => { if ( filter !== "mine" && squareTabs.length > 0 && !squareTabs.some((sq) => sq.id === filter) ) { setFilter("mine"); } }, [filter, squareTabs]); const pagedSkills = useMemo( () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), [orderedSkills, page], ); const handleFillNames = async () => { try { const result = await fillNamesMutation.mutateAsync(fillModel); 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 if (result.model_error) { // Failures came from the model itself — say so explicitly. toast.warning( `已补充 ${okCount} 个,${failCount} 个因大模型报错失败:${result.model_error}`, ); } 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(); if (result.adopted.length > 0) { toast.success( t.settings.skills.rescanSkillsAdopted.replace( "{count}", String(result.adopted.length), ), theme === "dark" ? rescanSuccessToastDarkOptions : undefined, ); } else { toast.info( t.settings.skills.rescanSkillsNoop, theme === "dark" ? rescanInfoToastDarkOptions : undefined, ); } } catch (err) { toast.error( err instanceof Error ? err.message : String(err), theme === "dark" ? rescanErrorToastDarkOptions : undefined, ); } }; return (
{t.settings.skills.tabMine} {squareTabs.map((sq) => ( {sq.name} ))}
{isAdmin && ( <> )}
} /> {!isLoading && !error && (
共 {orderedSkills.length} 个技能
)} {publisher && (
筛选发布者:
)} {isLoading && (
{t.common.loading}
)} {error &&
Error: {error.message}
} {!isLoading && !error && filteredSkills.length === 0 && ( )} {!isLoading && !error && filteredSkills.length > 0 && (
{pagedSkills.map((skill) => ( ))}
)} {!isLoading && !error && totalPages > 1 && (
{t.settings.skills.paginationPageOf .replace("{page}", String(page)) .replace("{total}", String(totalPages))}
)} setInstallOpen(false)} isAdmin={isAdmin} /> ); } export function SkillRow({ skill, currentUserId, isAdmin, layout = "row", onPublisherClick, knowledgeBindings = [], }: { 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; /** Admin list can inject its single batched binding query here (never N+1). */ knowledgeBindings?: SkillKnowledgeBinding[]; }) { const isCard = layout === "card"; const { t } = useI18n(); const setPublished = useSetSkillPublished(); const { data: cardPublishSquares } = usePublishSquares(); const setPinned = useSetSkillPinned(); const setFeatured = useSetSkillFeatured(); const setAlwaysOn = useSetSkillAlwaysOn(); const { data: compression } = useSkillCompressionSettings(); const deleteCustom = useDeleteCustomSkill(); const deleteBuiltin = useDeleteBuiltinSkill(); const duplicateSkillMutation = useDuplicateSkill(); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [tagPickerOpen, setTagPickerOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false); const [duplicateOpen, setDuplicateOpen] = useState(false); const [duplicateName, setDuplicateName] = useState(""); const [publishPickerOpen, setPublishPickerOpen] = useState(false); const isCustom = skill.category === "custom"; // Built-in flag falls back to "no DB owner" so legacy responses without // the multitenancy fields still classify public skills correctly. const isBuiltin = skill.builtin ?? !skill.owner_user_id; const isOwned = isCustom && !isBuiltin && !!skill.owner_user_id && skill.owner_user_id === currentUserId; const isFromOthers = isCustom && !isBuiltin && !isOwned; // Admin can manage custom skills from any owner; non-admin only their own. const canManage = isCustom && (isOwned || isAdmin); const canDelete = isAdmin || isOwned; const canTogglePublish = canManage && !isBuiltin; // 常驻(免压缩):内置技能仅管理员可设;自定义技能其所有者或管理员可设(与后端一致)。 const canSetAlwaysOn = isAdmin || isOwned; async function handleToggleAlwaysOn(next: boolean) { try { await setAlwaysOn.mutateAsync({ skillName: skill.name, alwaysOn: next }); toast.success(next ? "已设为常驻(免压缩)" : "已取消常驻"); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } async function handleTogglePublished(next: boolean) { // Publishing: prompt for which square to publish into (when configured). // Unpublishing: just flip it off. if (next && (cardPublishSquares?.squares.length ?? 0) > 0) { setPublishPickerOpen(true); return; } try { await setPublished.mutateAsync({ skillName: skill.name, published: next }); toast.success( next ? t.settings.skills.publishSuccess : t.settings.skills.unpublishSuccess, ); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } // Publish the skill into a chosen square. Each skill belongs to exactly one // square; re-publishing with a new square_id relocates rather than duplicates. async function handlePublishToSquare(squareId: string) { try { await setPublished.mutateAsync({ skillName: skill.name, published: true, squareId, }); const sqName = cardPublishSquares?.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() { try { if (isBuiltin) { await deleteBuiltin.mutateAsync(skill.name); } else { await deleteCustom.mutateAsync(skill.name); } toast.success(t.settings.skills.deleteSuccess); setConfirmDeleteOpen(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } function handleOpenDuplicate() { // Suggest a "-copy" default that already satisfies the backend's // lowercase/digit/-/_ skill-name rule. The user can edit it before saving. setDuplicateName(`${skill.name}-copy`); setDuplicateOpen(true); } async function handleDuplicate() { const target = duplicateName.trim(); if (!target) { toast.error("请输入新技能名称"); return; } try { const result = await duplicateSkillMutation.mutateAsync({ sourceName: skill.name, newName: target, }); toast.success(`已复制为「${result.skill_name}」`); setDuplicateOpen(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } async function handleTogglePinned() { const next = !skill.pinned; try { await setPinned.mutateAsync({ skillName: skill.name, pinned: next }); toast.success( next ? t.settings.skills.pinSuccess : t.settings.skills.unpinSuccess, ); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } async function handleToggleFeatured() { const next = !skill.featured; try { await setFeatured.mutateAsync({ skillName: skill.name, featured: next }); toast.success( next ? t.settings.skills.featureSuccess : t.settings.skills.unfeatureSuccess, ); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } const badgeGroup = (
{skill.name_zh?.trim() ? skill.name_zh : desensitize(skill.name)} {skill.name_zh?.trim() && ( {desensitize(skill.name)} )} {isBuiltin && ( {t.settings.skills.badgeBuiltin} )} {isOwned && ( {t.settings.skills.badgeMine} )} {isFromOthers && ( {t.settings.skills.badgeFromOthers} )} {(isOwned || isAdmin) && !isBuiltin && skill.published && ( {t.settings.skills.badgePublished} )} {(isOwned || isAdmin) && !isBuiltin && !skill.published && ( {t.settings.skills.badgeUnpublished} )} {isCustom && !isBuiltin && skill.source === "agent" && ( {t.settings.skills.badgeSourceAgent} )} {isCustom && !isBuiltin && skill.source === "upload" && ( {t.settings.skills.badgeSourceUpload} )} {skill.featured && ( {t.settings.skills.badgeFeatured} )} {skill.pinned && ( {t.settings.skills.badgePinned} )} {skill.always_on && ( 常驻 )} {knowledgeBindings.slice(0, 3).map((binding) => ( 知识库:{binding.target_name || binding.target_id} {binding.status === "stale" ? "(待更新)" : ""} ))} {knowledgeBindings.length > 3 && ( +{knowledgeBindings.length - 3} )}
); const metaNodes = ( <> {!isBuiltin && skill.owner_username && ( )} {skill.use_count !== undefined && (
{skill.use_count > 0 ? t.settings.skills.useCountInline.replace( "{count}", String(skill.use_count), ) : t.settings.skills.useCountInlineZero}
)} {formatDateTime(skill.created_at) && (
创建时间:{formatDateTime(skill.created_at)}
)} {(skill.tags?.length ?? 0) > 0 && ( )} ); const actionButtons = ( <> {canManage && ( )} {canManage && ( )} {compression?.enabled && canSetAlwaysOn && ( )} {canTogglePublish && ( )} {canDelete && ( )} ); return ( <> {isCard ? (
setDetailOpen(true)} > {badgeGroup}

{desensitize(skill.description ?? "")}

{metaNodes}
{actionButtons}
) : ( setDetailOpen(true)} > {badgeGroup} {desensitize(skill.description ?? "")} {metaNodes} {actionButtons} )} 复制技能 将「{desensitize(skill.name)}」复制为一个属于你的新技能。请输入新名称(仅小写字母、数字、- 或 _)。 setDuplicateName(e.currentTarget.value)} placeholder="new-skill-name" autoFocus onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void handleDuplicate(); } }} /> {t.settings.skills.deleteAction} {desensitize( t.settings.skills.deleteConfirm.replace("{name}", skill.name), )} void handlePublishToSquare(squareId)} description="请选择要把该技能发布到哪个广场。每个技能只能归属一个广场,不会重复出现。" /> ); } function EmptySkill() { const { t } = useI18n(); return ( {t.settings.skills.emptyTitle} {t.settings.skills.emptyDescription} ); } function InstallSkillDialog({ open, onClose, isAdmin, }: { open: boolean; onClose: () => void; isAdmin: boolean; }) { const { t } = useI18n(); const dialog = t.settings.skills.installDialog; const inputRef = useRef(null); const [file, setFile] = useState(null); const [validation, setValidation] = useState(null); const [validationError, setValidationError] = useState(null); const [isDragging, setIsDragging] = useState(false); const [targetCategory, setTargetCategory] = useState<"custom" | "public">("custom"); // After a successful install the dialog switches to a "set Chinese name" step. // `installedName` holds the just-installed skill name (null = still on the upload step). const [installedName, setInstalledName] = useState(null); const [nameZhDraft, setNameZhDraft] = useState(""); // Optional model override for the "AI generate" button (null = configured default). const [nameZhModel, setNameZhModel] = useState(null); const validateMutation = useValidateSkillUpload(); const installMutation = useInstallSkillUpload(); const setNameZhMutation = useSetSkillNameZh(); const generateNameZhMutation = useGenerateSkillNameZh(); const validateFile = useCallback( async (selected: File, category: "custom" | "public") => { setValidation(null); setValidationError(null); try { const result = await validateMutation.mutateAsync({ file: selected, targetCategory: category, }); setValidation(result); } catch (err) { setValidationError(err instanceof Error ? err.message : String(err)); } }, [validateMutation], ); const reset = useCallback(() => { setFile(null); setValidation(null); setValidationError(null); setIsDragging(false); setTargetCategory("custom"); setInstalledName(null); setNameZhDraft(""); setNameZhModel(null); if (inputRef.current) inputRef.current.value = ""; }, []); const handleClose = useCallback(() => { if ( validateMutation.isPending || installMutation.isPending || setNameZhMutation.isPending || generateNameZhMutation.isPending ) return; reset(); onClose(); }, [ generateNameZhMutation.isPending, installMutation.isPending, onClose, reset, setNameZhMutation.isPending, validateMutation.isPending, ]); const acceptFile = useCallback( async (selected: File) => { const lower = selected.name.toLowerCase(); if (!lower.endsWith(".zip") && !lower.endsWith(".skill")) { toast.error(dialog.rejectInvalidExt); return; } setFile(selected); await validateFile(selected, targetCategory); }, [dialog.rejectInvalidExt, targetCategory, validateFile], ); const onDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); // Stop the global chat-upload listener on `document` from also receiving this drop. // React's stopPropagation alone doesn't reach native document listeners — we need to // halt the underlying DOM event too. e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); setIsDragging(false); const dropped = e.dataTransfer.files?.[0]; if (dropped) void acceptFile(dropped); }, [acceptFile], ); const onPick = useCallback( (e: React.ChangeEvent) => { const picked = e.target.files?.[0]; if (picked) void acceptFile(picked); }, [acceptFile], ); const handleInstall = useCallback( async (overwrite: boolean) => { if (!file) return; try { const result = await installMutation.mutateAsync({ file, overwrite, targetCategory }); toast.success( (result.overwritten ? dialog.installSuccessReplaced : dialog.installSuccess).replace( "{name}", result.skill_name, ), ); // Stay open and advance to the optional "set Chinese name" step. setInstalledName(result.skill_name); setNameZhDraft(""); setNameZhModel(null); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, [dialog.installSuccess, dialog.installSuccessReplaced, file, installMutation, targetCategory], ); const handleGenerateNameZh = useCallback(async () => { if (!installedName) return; try { const result = await generateNameZhMutation.mutateAsync({ skillName: installedName, modelName: nameZhModel, }); setNameZhDraft(result.name_zh ?? ""); toast.success(dialog.nameZhGenerated); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, [dialog.nameZhGenerated, generateNameZhMutation, installedName, nameZhModel]); const handleSaveNameZh = useCallback(async () => { if (!installedName) return; const trimmed = nameZhDraft.trim(); try { // Empty draft = keep whatever is stored (or none); just close. if (trimmed) { await setNameZhMutation.mutateAsync({ skillName: installedName, nameZh: trimmed }); toast.success(dialog.nameZhSaved); } reset(); onClose(); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } }, [dialog.nameZhSaved, installedName, nameZhDraft, onClose, reset, setNameZhMutation]); const isPublicConflict = validation?.conflict && validation.conflict_kind === "public"; const isCustomConflict = validation?.conflict && validation.conflict_kind === "custom"; // Admin targeting public directory can overwrite an existing public skill. const adminCanOverwritePublic = isAdmin && targetCategory === "public" && !!isPublicConflict; const canInstall = !!validation && !validationError && validation.can_install_as_is; const canOverwrite = !!validation && !validationError && validation.can_overwrite; const ownCustomCanOverwrite = !!isCustomConflict && !!validation?.owned_by_me && canOverwrite; const otherCustomConflict = !!isCustomConflict && !!validation?.owned_by_other; const customConflictMessage = !validation ? "" : otherCustomConflict ? dialog.conflictOwnedByOther .replace("{name}", validation.skill_name) .replace( "{owner}", validation.owner_username ?? validation.owner_user_id ?? "—", ) : validation.owned_by_me ? (ownCustomCanOverwrite ? dialog.conflictOwnedByMe : dialog.conflictOwnedByMeWrongTarget ).replace("{name}", validation.skill_name) : dialog.conflictCustom.replace("{name}", validation.skill_name); return ( !o && handleClose()}> {installedName ? dialog.nameZhStepTitle : dialog.title} {installedName && ( <>

{dialog.nameZhStepHint.replace("{name}", desensitize(installedName))}

setNameZhDraft(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") void handleSaveNameZh(); }} placeholder={dialog.nameZhPlaceholder} className="h-9 flex-1" autoFocus disabled={setNameZhMutation.isPending || generateNameZhMutation.isPending} />
)} {!installedName && ( <>
{ e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={onDrop} className={cn( "flex flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed p-6 text-center transition-colors", isDragging ? "border-primary bg-accent/40" : "border-muted-foreground/30", )} >
{dialog.dropHint}
{file && (
{file.name} · {(file.size / 1024).toFixed(1)} KB
)}
{isAdmin && (
{dialog.targetCategoryLabel}
{(["custom", "public"] as const).map((cat) => ( ))}
)} {validateMutation.isPending && (
{dialog.validating}
)} {validationError && (
{validationError}
)} {validation && !validationError && (
{desensitize(validation.skill_name)}
{validation.description ? desensitize(validation.description) : "—"} {validation.license && ( {desensitize(validation.license)} )} {validation.allowed_tools.length > 0 && (
{validation.allowed_tools.map((tool) => ( {desensitize(tool)} ))}
)} {validation.warnings.length > 0 && (
    {validation.warnings.map((w, idx) => (
  • {desensitize(w)}
  • ))}
)} {isCustomConflict && (
{desensitize(customConflictMessage)}
)} {isPublicConflict && (
{desensitize( (adminCanOverwritePublic ? dialog.conflictPublicAdminHint : dialog.conflictPublic).replace("{name}", validation.skill_name), )}
)}
)}
{canOverwrite && ( )} )}
); } function Field({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{label}
{children}
); }