"use client"; import { useEffect, useRef, useState } from "react"; import { ImagePlus, LoaderCircle, Plus, Sparkles, Star, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { SkillCoverThumbnail } from "@/app/chat/components/skill-showcase"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { generateSkillExamples, updateSkillPresentation, uploadSkillPresentationAssets, type InstalledSkill, type SkillAsset, type SkillExample, type SkillInputSpec, type SkillRuntime } from "@/lib/api"; type Props = { skill: InstalledSkill | null; runtime: SkillRuntime | null; onOpenChange: (open: boolean) => void; onSaved: (runtime: SkillRuntime, items: InstalledSkill[]) => void; }; function emptyExample(index: number): SkillExample { return { id: `custom-${Date.now()}-${index}`, title: `示例 ${index + 1}`, description: "", prompt: "", assets: [], attachment_hint: "" }; } export function SkillPresentationEditor({ skill, runtime, onOpenChange, onSaved }: Props) { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [category, setCategory] = useState(""); const [tags, setTags] = useState(""); const [coverPath, setCoverPath] = useState(""); const [assets, setAssets] = useState([]); const [examples, setExamples] = useState([]); const [inputs, setInputs] = useState([]); const [working, setWorking] = useState<"" | "save" | "upload" | "generate">(""); const fileRef = useRef(null); useEffect(() => { if (!runtime) return; setTitle(runtime.presentation.title || runtime.name); setDescription(runtime.presentation.description || runtime.description); setCategory(runtime.presentation.category || "专业工作流"); setTags(runtime.presentation.tags.join("、")); setCoverPath(runtime.presentation.cover?.path || ""); setAssets(runtime.presentation.assets); setExamples(runtime.presentation.examples); setInputs(runtime.presentation.inputs); }, [runtime]); const save = async () => { if (!skill || !runtime || working) return; const validExamples = examples.filter((example) => example.prompt.trim()).map((example, index) => ({ ...example, id: example.id || `custom-${index + 1}`, title: example.title.trim() || `示例 ${index + 1}`, prompt: example.prompt.trim(), })); if (!validExamples.length) { toast.error("至少保留一个可用示例"); return; } setWorking("save"); try { const result = await updateSkillPresentation(skill.id, { display: { title: title.trim() || runtime.name, description: description.trim(), category: category.trim() || "专业工作流", cover: coverPath, tags: tags.split(/[,,、\n]/).map((item) => item.trim()).filter(Boolean), }, assets: assets.map((asset) => ({ path: asset.path, name: asset.name, description: asset.description, role: asset.path === coverPath ? "cover" : "gallery" })), examples: validExamples, inputs: inputs.filter((input) => input.label.trim()).map((input, index) => ({ ...input, id: input.id || `input-${index + 1}`, label: input.label.trim(), description: input.description.trim() })), }); onSaved(result.skill, result.items); toast.success("Skill 展示信息已保存"); onOpenChange(false); } catch (error) { toast.error(error instanceof Error ? error.message : "保存展示信息失败"); } finally { setWorking(""); } }; const upload = async (files: File[]) => { if (!skill || !files.length || working) return; setWorking("upload"); try { const result = await uploadSkillPresentationAssets(skill.id, files); setAssets((current) => [...current, ...result.assets.filter((asset) => !current.some((item) => item.path === asset.path))]); if (!coverPath && result.assets[0]) setCoverPath(result.assets[0].path); toast.success(`已添加 ${result.assets.length} 张展示图片,保存后生效`); } catch (error) { toast.error(error instanceof Error ? error.message : "上传展示图片失败"); } finally { if (fileRef.current) fileRef.current.value = ""; setWorking(""); } }; const generateExamples = async () => { if (!skill || working) return; setWorking("generate"); try { const result = await generateSkillExamples(skill.id); setExamples(result.skill.presentation.examples); onSaved(result.skill, result.items); toast.success("AI 已根据 Skill 文档生成示例,你可以继续编辑后保存"); } catch (error) { toast.error(error instanceof Error ? error.message : "AI 生成示例失败"); } finally { setWorking(""); } }; return ( 编辑 Skill 展示

后台设置会覆盖自动扫描结果,不会修改第三方 Skill 的原始文件。

{!runtime || !skill ? (
正在读取展示信息…
) : (