Files
tkmind_go/documentation/src/pages/skills/index.tsx
T
2026-02-11 18:04:46 +00:00

225 lines
8.8 KiB
TypeScript

import { SkillCard } from "@site/src/components/skill-card";
import { searchSkills, getAllTags } from "@site/src/utils/skills";
import type { Skill } from "@site/src/pages/skills/types";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Layout from "@theme/Layout";
import Admonition from "@theme/Admonition";
import { Button } from "@site/src/components/ui/button";
import { SidebarFilter, type SidebarFilterGroup } from "@site/src/components/ui/sidebar-filter";
import { Menu, X } from "lucide-react";
import Link from '@docusaurus/Link';
export default function SkillsPage() {
const [skills, setSkills] = useState<Skill[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const skillsPerPage = 10;
// Build tag filter options from loaded skills
const uniqueTags = Array.from(
new Set(
skills.flatMap((s) => s.tags || [])
)
).sort().map((tag) => ({
label: tag.charAt(0).toUpperCase() + tag.slice(1),
value: tag
}));
// Build source filter options (Community only - official is the default)
const sourceOptions = [
{ label: "Community", value: "community" }
];
const sidebarFilterGroups: SidebarFilterGroup[] = [
{
title: "Source",
options: sourceOptions
},
{
title: "Tags",
options: uniqueTags,
maxHeight: "max-h-64 overflow-y-auto"
}
];
useEffect(() => {
const loadSkills = async () => {
try {
setIsLoading(true);
setError(null);
const results = await searchSkills(searchQuery);
setSkills(results);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
setError(`Failed to load skills: ${errorMessage}`);
console.error("Error loading skills:", err);
} finally {
setIsLoading(false);
}
};
const timeoutId = setTimeout(loadSkills, 300);
return () => clearTimeout(timeoutId);
}, [searchQuery]);
// Apply filters
let filteredSkills = skills;
Object.entries(selectedFilters).forEach(([group, values]) => {
if (values.length > 0) {
filteredSkills = filteredSkills.filter((skill) => {
if (group === "Tags") {
return skill.tags?.some((tag) => values.includes(tag)) ?? false;
}
if (group === "Source") {
// Use isCommunity field from manifest (true if author is not "goose")
const isCommunity = skill.isCommunity ?? false;
if (values.includes("community")) return isCommunity;
return true;
}
return true;
});
}
});
return (
<Layout
title="Skills Marketplace"
description="Browse and install community-contributed skills for goose"
>
<div className="container mx-auto px-4 py-8 md:p-24">
<div className="pb-8 md:pb-16">
<div className="flex justify-between items-start mb-4">
<h1 className="text-4xl md:text-[64px] font-medium text-textProminent">
Skills Marketplace
</h1>
<Button
onClick={() => window.open('https://github.com/block/Agent-Skills?tab=readme-ov-file#contributing-a-skill', '_blank')}
className="bg-purple-600 hover:bg-purple-700 text-white flex items-center gap-2 cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14M5 12h14"/>
</svg>
Submit Skill
</Button>
</div>
<p className="text-textProminent">
Browse community-contributed{" "}
<Link to="/docs/guides/context-engineering/using-skills" className="text-purple-600 hover:underline">
skills
</Link>{" "}
that teach goose how to perform specific tasks. Skills are reusable instruction sets with optional supporting files.
</p>
</div>
<div className="search-container mb-6 md:mb-8">
<input
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-2 md:py-3 text-2xl md:text-[40px] leading-tight md:leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
placeholder="Search skills by name, description, or tag"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<div className="md:hidden mb-4">
<Button onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}>
{isMobileFilterOpen ? <X size={20} /> : <Menu size={20} />}
{isMobileFilterOpen ? "Close Filters" : "Show Filters"}
</Button>
</div>
<div className="flex flex-col md:flex-row gap-8">
<div className={`${isMobileFilterOpen ? "block" : "hidden"} md:block md:w-64 mt-6`}>
<SidebarFilter
groups={sidebarFilterGroups}
selectedValues={selectedFilters}
onChange={(group, values) => {
setSelectedFilters(prev => ({ ...prev, [group]: values }));
setCurrentPage(1);
}}
/>
</div>
<div className="flex-1">
<div className={`${searchQuery ? "pb-2" : "pb-4 md:pb-8"}`}>
<p className="text-gray-600">
{searchQuery
? `${filteredSkills.length} result${filteredSkills.length !== 1 ? "s" : ""} for "${searchQuery}"`
: `${filteredSkills.length} skill${filteredSkills.length !== 1 ? "s" : ""} available`}
</p>
</div>
{error && (
<Admonition type="danger" title="Error">
<p>{error}</p>
</Admonition>
)}
{isLoading ? (
<div className="py-8 text-xl text-gray-600">Loading skills...</div>
) : filteredSkills.length === 0 ? (
<Admonition type="info">
<p>
{searchQuery
? "No skills found matching your search."
: "No skills have been submitted yet."}
</p>
</Admonition>
) : (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
{filteredSkills
.slice((currentPage - 1) * skillsPerPage, currentPage * skillsPerPage)
.map((skill) => (
<motion.div
key={skill.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.6 }}
>
<SkillCard skill={skill} />
</motion.div>
))}
</div>
{filteredSkills.length > skillsPerPage && (
<div className="flex justify-center items-center gap-2 md:gap-4 mt-6 md:mt-8">
<Button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
>
Previous
</Button>
<span className="text-textProminent text-sm md:text-base">
Page {currentPage} of {Math.ceil(filteredSkills.length / skillsPerPage)}
</span>
<Button
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(filteredSkills.length / skillsPerPage), prev + 1))}
disabled={currentPage >= Math.ceil(filteredSkills.length / skillsPerPage)}
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
>
Next
</Button>
</div>
)}
</>
)}
</div>
</div>
</div>
</Layout>
);
}