import { useState, useEffect, useMemo, forwardRef, useImperativeHandle, } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { RefreshCw, Search } from "lucide-react"; import { toast } from "sonner"; import { SkillCard } from "./SkillCard"; import { RepoManagerPanel } from "./RepoManagerPanel"; import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills"; import { formatSkillError } from "@/lib/errors/skillErrorParser"; interface SkillsPageProps { onClose?: () => void; } export interface SkillsPageHandle { refresh: () => void; openRepoManager: () => void; } export const SkillsPage = forwardRef( ({ onClose: _onClose }, ref) => { const { t } = useTranslation(); const [skills, setSkills] = useState([]); const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); const [repoManagerOpen, setRepoManagerOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const loadSkills = async (afterLoad?: (data: Skill[]) => void) => { try { setLoading(true); const data = await skillsApi.getAll(); setSkills(data); if (afterLoad) { afterLoad(data); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 传入 "skills.loadFailed" 作为标题 const { title, description } = formatSkillError( errorMessage, t, "skills.loadFailed", ); toast.error(title, { description, duration: 8000, }); console.error("Load skills failed:", error); } finally { setLoading(false); } }; const loadRepos = async () => { try { const data = await skillsApi.getRepos(); setRepos(data); } catch (error) { console.error("Failed to load repos:", error); } }; useEffect(() => { Promise.all([loadSkills(), loadRepos()]); }, []); useImperativeHandle(ref, () => ({ refresh: () => loadSkills(), openRepoManager: () => setRepoManagerOpen(true), })); const handleInstall = async (directory: string) => { try { await skillsApi.install(directory); toast.success(t("skills.installSuccess", { name: directory })); await loadSkills(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 使用错误解析器格式化错误,传入 "skills.installFailed" const { title, description } = formatSkillError( errorMessage, t, "skills.installFailed", ); toast.error(title, { description, duration: 10000, // 延长显示时间让用户看清 }); console.error("Install skill failed:", { directory, error, message: errorMessage, }); } }; const handleUninstall = async (directory: string) => { try { await skillsApi.uninstall(directory); toast.success(t("skills.uninstallSuccess", { name: directory })); await loadSkills(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 使用错误解析器格式化错误,传入 "skills.uninstallFailed" const { title, description } = formatSkillError( errorMessage, t, "skills.uninstallFailed", ); toast.error(title, { description, duration: 10000, }); console.error("Uninstall skill failed:", { directory, error, message: errorMessage, }); } }; const handleAddRepo = async (repo: SkillRepo) => { await skillsApi.addRepo(repo); let repoSkillCount = 0; await Promise.all([ loadRepos(), loadSkills((data) => { repoSkillCount = data.filter( (skill) => skill.repoOwner === repo.owner && skill.repoName === repo.name && (skill.repoBranch || "main") === (repo.branch || "main"), ).length; }), ]); toast.success( t("skills.repo.addSuccess", { owner: repo.owner, name: repo.name, count: repoSkillCount, }), ); }; const handleRemoveRepo = async (owner: string, name: string) => { await skillsApi.removeRepo(owner, name); toast.success(t("skills.repo.removeSuccess", { owner, name })); await Promise.all([loadRepos(), loadSkills()]); }; // 过滤技能列表 const filteredSkills = useMemo(() => { if (!searchQuery.trim()) return skills; const query = searchQuery.toLowerCase(); return skills.filter((skill) => { const name = skill.name?.toLowerCase() || ""; const description = skill.description?.toLowerCase() || ""; const directory = skill.directory?.toLowerCase() || ""; return ( name.includes(query) || description.includes(query) || directory.includes(query) ); }); }, [skills, searchQuery]); return (
{/* 顶部操作栏(固定区域)已移除,由 App.tsx 接管 */} {/* 技能网格(可滚动详情区域) */}
{loading ? (
) : skills.length === 0 ? (

{t("skills.empty")}

{t("skills.emptyDescription")}

) : ( <> {/* 搜索框 */}
setSearchQuery(e.target.value)} className="pl-9" />
{searchQuery && (

{t("skills.count", { count: filteredSkills.length })}

)}
{/* 技能列表或无结果提示 */} {filteredSkills.length === 0 ? (

{t("skills.noResults")}

{t("skills.emptyDescription")}

) : (
{filteredSkills.map((skill) => ( ))}
)} )}
{/* 仓库管理面板 */} {repoManagerOpen && ( setRepoManagerOpen(false)} /> )}
); }, ); SkillsPage.displayName = "SkillsPage";