import { useEffect, useMemo, useRef, useState } from 'react'; import { FileCode2, FileText, MoreVertical, Pencil, Plus, Search, Sparkles, Trash2, Upload, X, } from 'lucide-react'; import { useTheme } from '../../contexts/ThemeContext'; interface FileSkill { id: string; folder: string; name: string; description: string; kind: 'md' | 'py'; filename: string; created_at: number; updated_at: number; } const API_BASE = 'https://ch1.b.uat.4.cn/zncmback/api/skills/files'; export default function FileSkillsWorkshop() { const { theme } = useTheme(); const isDark = theme === 'dark'; const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [createOpen, setCreateOpen] = useState(false); const [editing, setEditing] = useState(null); const [openCardMenuId, setOpenCardMenuId] = useState(null); const cardMenuRef = useRef(null); const reload = async () => { setLoading(true); setError(''); try { const res = await fetch(API_BASE); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); const data = (await res.json()) as FileSkill[]; setSkills(data); } catch (err) { console.error('加载技能失败', err); setError('加载技能失败,请稍后重试'); setSkills([]); } finally { setLoading(false); } }; useEffect(() => { void reload(); }, []); useEffect(() => { const handle = (event: MouseEvent) => { const target = event.target as Node; if (cardMenuRef.current && !cardMenuRef.current.contains(target)) { setOpenCardMenuId(null); } }; document.addEventListener('mousedown', handle); return () => document.removeEventListener('mousedown', handle); }, []); const filtered = useMemo(() => { const keyword = searchTerm.trim().toLowerCase(); if (!keyword) return skills; return skills.filter((s) => `${s.name} ${s.description} ${s.filename}`.toLowerCase().includes(keyword), ); }, [searchTerm, skills]); const handleDelete = async (skill: FileSkill) => { setOpenCardMenuId(null); if (!window.confirm(`确认删除技能「${skill.name}」?`)) return; try { const res = await fetch(`${API_BASE}/${encodeURIComponent(skill.folder)}`, { method: 'DELETE', }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); setSkills((prev) => prev.filter((s) => s.folder !== skill.folder)); } catch (err) { console.error('删除技能失败', err); setError('删除技能失败'); } }; return (
{createOpen && ( setCreateOpen(false)} onCreated={() => { setCreateOpen(false); void reload(); }} /> )} {editing && ( setEditing(null)} onSaved={() => { setEditing(null); void reload(); }} /> )}

技能管理

上传 Markdown 提示词或 Python 脚本作为技能,供 MCP 与模型调用。

setSearchTerm(event.target.value)} className={`min-w-0 flex-1 bg-transparent text-sm outline-none ${ isDark ? 'text-slate-100 placeholder:text-slate-500' : 'text-gray-800 placeholder:text-[#9ca3af]' }`} />
{loading ? (
正在加载技能...
) : error ? (

加载失败

{error}

) : filtered.length === 0 ? (

暂无技能

点击右上角「新建技能」上传你的第一个技能。

) : (
{filtered.map((skill) => { const isPy = skill.kind === 'py'; const Icon = isPy ? FileCode2 : FileText; return (
{openCardMenuId === skill.folder && (
)}

{skill.name}

{skill.description || '暂无描述'}

{isPy ? 'Python' : 'Markdown'} {new Date(skill.updated_at * 1000).toLocaleString()}
); })}
)}
); } // --------------------------------------------------------------------------- // Create modal // --------------------------------------------------------------------------- interface CreateModalProps { isDark: boolean; onClose: () => void; onCreated: () => void; } function CreateSkillModal({ isDark, onClose, onCreated }: CreateModalProps) { const [file, setFile] = useState(null); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [submitting, setSubmitting] = useState(false); const [errMsg, setErrMsg] = useState(''); const inputRef = useRef(null); const handleFileChange = (next: File | null) => { setFile(next); if (next && !name.trim()) { const stem = next.name.replace(/\.[^.]+$/, ''); setName(stem); } }; const submit = async () => { if (!file) { setErrMsg('请上传 .md 或 .py 文件'); return; } setErrMsg(''); setSubmitting(true); try { const fd = new FormData(); fd.append('file', file); if (name.trim()) fd.append('name', name.trim()); if (description.trim()) fd.append('description', description.trim()); const res = await fetch(API_BASE, { method: 'POST', body: fd }); if (!res.ok) { const text = await res.text(); throw new Error(text || `${res.status} ${res.statusText}`); } onCreated(); } catch (err) { console.error('新建技能失败', err); setErrMsg(err instanceof Error ? err.message : '新建技能失败'); } finally { setSubmitting(false); } }; return (
{ if (event.target === event.currentTarget) onClose(); }} >

新建技能

handleFileChange(event.target.files?.[0] ?? null)} />
setName(event.target.value)} placeholder="默认使用上传文件名" className={`w-full rounded-2xl border px-4 py-2.5 text-sm outline-none transition ${ isDark ? 'border-slate-700 bg-slate-950/40 text-slate-100 placeholder:text-slate-500 focus:border-slate-500' : 'border-[#f5d4d4] bg-white text-gray-900 placeholder:text-gray-400 focus:border-[#ffbdbd]' }`} />