mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Add multi-select filters, light/dark theme, and skill ZIP downloads
- Add multi-select dropdown component for all filter fields - Implement light/dark theme toggle with system preference detection - Add client-side ZIP download for skills using JSZip - Include file lists in skills metadata for download feature - Add title tooltips to multi-select options for long values - Update all pages with consistent theme toggle in header
This commit is contained in:
@@ -80,17 +80,34 @@ function generateAgentsData() {
|
||||
.readdirSync(AGENTS_DIR)
|
||||
.filter((f) => f.endsWith(".agent.md"));
|
||||
|
||||
// Track all unique values for filters
|
||||
const allModels = new Set();
|
||||
const allTools = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(AGENTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
const model = frontmatter?.model || null;
|
||||
const tools = frontmatter?.tools || [];
|
||||
const handoffs = frontmatter?.handoffs || [];
|
||||
|
||||
// Track unique values
|
||||
if (model) allModels.add(model);
|
||||
tools.forEach((t) => allTools.add(t));
|
||||
|
||||
agents.push({
|
||||
id: file.replace(".agent.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
model: frontmatter?.model || null,
|
||||
tools: frontmatter?.tools || [],
|
||||
model: model,
|
||||
tools: tools,
|
||||
hasHandoffs: handoffs.length > 0,
|
||||
handoffs: handoffs.map((h) => ({
|
||||
label: h.label || "",
|
||||
agent: h.agent || "",
|
||||
})),
|
||||
mcpServers: frontmatter?.["mcp-servers"]
|
||||
? Object.keys(frontmatter["mcp-servers"])
|
||||
: [],
|
||||
@@ -99,7 +116,16 @@ function generateAgentsData() {
|
||||
});
|
||||
}
|
||||
|
||||
return agents.sort((a, b) => a.title.localeCompare(b.title));
|
||||
// Sort and return with filter metadata
|
||||
const sortedAgents = agents.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedAgents,
|
||||
filters: {
|
||||
models: ["(none)", ...Array.from(allModels).sort()],
|
||||
tools: Array.from(allTools).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,24 +137,73 @@ function generatePromptsData() {
|
||||
.readdirSync(PROMPTS_DIR)
|
||||
.filter((f) => f.endsWith(".prompt.md"));
|
||||
|
||||
// Track all unique tools for filters
|
||||
const allTools = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(PROMPTS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
const tools = frontmatter?.tools || [];
|
||||
tools.forEach((t) => allTools.add(t));
|
||||
|
||||
prompts.push({
|
||||
id: file.replace(".prompt.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
agent: frontmatter?.agent || null,
|
||||
model: frontmatter?.model || null,
|
||||
tools: frontmatter?.tools || [],
|
||||
tools: tools,
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
return prompts.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const sortedPrompts = prompts.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedPrompts,
|
||||
filters: {
|
||||
tools: Array.from(allTools).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse applyTo field into an array of patterns
|
||||
*/
|
||||
function parseApplyToPatterns(applyTo) {
|
||||
if (!applyTo) return [];
|
||||
|
||||
// Handle array format
|
||||
if (Array.isArray(applyTo)) {
|
||||
return applyTo.map(p => p.trim()).filter(p => p.length > 0);
|
||||
}
|
||||
|
||||
// Handle string format (comma-separated)
|
||||
if (typeof applyTo === 'string') {
|
||||
return applyTo.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract file extension from a glob pattern
|
||||
*/
|
||||
function extractExtensionFromPattern(pattern) {
|
||||
// Match patterns like **.ts, **/*.js, *.py, etc.
|
||||
const match = pattern.match(/\*\.(\w+)$/);
|
||||
if (match) return `.${match[1]}`;
|
||||
|
||||
// Match patterns like **/*.{ts,tsx}
|
||||
const braceMatch = pattern.match(/\*\.\{([^}]+)\}$/);
|
||||
if (braceMatch) {
|
||||
return braceMatch[1].split(',').map(ext => `.${ext.trim()}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,22 +215,75 @@ function generateInstructionsData() {
|
||||
.readdirSync(INSTRUCTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".instructions.md"));
|
||||
|
||||
// Track all unique patterns and extensions for filters
|
||||
const allPatterns = new Set();
|
||||
const allExtensions = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(INSTRUCTIONS_DIR, file);
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
const applyToRaw = frontmatter?.applyTo || null;
|
||||
const applyToPatterns = parseApplyToPatterns(applyToRaw);
|
||||
|
||||
// Extract extensions from patterns
|
||||
const extensions = [];
|
||||
for (const pattern of applyToPatterns) {
|
||||
allPatterns.add(pattern);
|
||||
const ext = extractExtensionFromPattern(pattern);
|
||||
if (ext) {
|
||||
if (Array.isArray(ext)) {
|
||||
ext.forEach(e => {
|
||||
extensions.push(e);
|
||||
allExtensions.add(e);
|
||||
});
|
||||
} else {
|
||||
extensions.push(ext);
|
||||
allExtensions.add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instructions.push({
|
||||
id: file.replace(".instructions.md", ""),
|
||||
title: extractTitle(filePath, frontmatter),
|
||||
description: frontmatter?.description || "",
|
||||
applyTo: frontmatter?.applyTo || null,
|
||||
applyTo: applyToRaw,
|
||||
applyToPatterns: applyToPatterns,
|
||||
extensions: [...new Set(extensions)],
|
||||
path: relativePath,
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
return instructions.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const sortedInstructions = instructions.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedInstructions,
|
||||
filters: {
|
||||
patterns: Array.from(allPatterns).sort(),
|
||||
extensions: ["(none)", ...Array.from(allExtensions).sort()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize a skill based on its name and description
|
||||
*/
|
||||
function categorizeSkill(name, description) {
|
||||
const text = `${name} ${description}`.toLowerCase();
|
||||
|
||||
if (text.includes('azure') || text.includes('appinsights')) return 'Azure';
|
||||
if (text.includes('github') || text.includes('gh-cli') || text.includes('git-commit') || text.includes('git ')) return 'Git & GitHub';
|
||||
if (text.includes('vscode') || text.includes('vs code')) return 'VS Code';
|
||||
if (text.includes('test') || text.includes('qa') || text.includes('playwright')) return 'Testing';
|
||||
if (text.includes('microsoft') || text.includes('m365') || text.includes('workiq')) return 'Microsoft';
|
||||
if (text.includes('cli') || text.includes('command')) return 'CLI Tools';
|
||||
if (text.includes('diagram') || text.includes('plantuml') || text.includes('visual')) return 'Diagrams';
|
||||
if (text.includes('nuget') || text.includes('dotnet') || text.includes('.net')) return '.NET';
|
||||
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,19 +293,26 @@ function generateSkillsData() {
|
||||
const skills = [];
|
||||
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
return skills;
|
||||
return { items: [], filters: { categories: [], hasAssets: ['Yes', 'No'] } };
|
||||
}
|
||||
|
||||
const folders = fs
|
||||
.readdirSync(SKILLS_DIR)
|
||||
.filter((f) => fs.statSync(path.join(SKILLS_DIR, f)).isDirectory());
|
||||
|
||||
const allCategories = new Set();
|
||||
|
||||
for (const folder of folders) {
|
||||
const skillPath = path.join(SKILLS_DIR, folder);
|
||||
const metadata = parseSkillMetadata(skillPath);
|
||||
|
||||
if (metadata) {
|
||||
const relativePath = path.relative(ROOT_FOLDER, skillPath).replace(/\\/g, "/");
|
||||
const category = categorizeSkill(metadata.name, metadata.description);
|
||||
allCategories.add(category);
|
||||
|
||||
// Get all files in the skill folder recursively
|
||||
const files = getSkillFiles(skillPath, relativePath);
|
||||
|
||||
skills.push({
|
||||
id: folder,
|
||||
@@ -188,13 +323,55 @@ function generateSkillsData() {
|
||||
.join(" "),
|
||||
description: metadata.description,
|
||||
assets: metadata.assets,
|
||||
hasAssets: metadata.assets.length > 0,
|
||||
assetCount: metadata.assets.length,
|
||||
category: category,
|
||||
path: relativePath,
|
||||
skillFile: `${relativePath}/SKILL.md`,
|
||||
files: files,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return skills.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const sortedSkills = skills.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {
|
||||
items: sortedSkills,
|
||||
filters: {
|
||||
categories: Array.from(allCategories).sort(),
|
||||
hasAssets: ['Yes', 'No'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a skill folder recursively
|
||||
*/
|
||||
function getSkillFiles(skillPath, relativePath) {
|
||||
const files = [];
|
||||
|
||||
function walkDir(dir, relDir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath, relPath);
|
||||
} else {
|
||||
// Get file size
|
||||
const stats = fs.statSync(fullPath);
|
||||
files.push({
|
||||
path: `${relativePath}/${relPath}`,
|
||||
name: relPath,
|
||||
size: stats.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(skillPath, '');
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,17 +388,23 @@ function generateCollectionsData() {
|
||||
.readdirSync(COLLECTIONS_DIR)
|
||||
.filter((f) => f.endsWith(".collection.yml"));
|
||||
|
||||
// Track all unique tags
|
||||
const allTags = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||
const data = parseCollectionYaml(filePath);
|
||||
const relativePath = path.relative(ROOT_FOLDER, filePath).replace(/\\/g, "/");
|
||||
|
||||
if (data) {
|
||||
const tags = data.tags || [];
|
||||
tags.forEach((t) => allTags.add(t));
|
||||
|
||||
collections.push({
|
||||
id: file.replace(".collection.yml", ""),
|
||||
name: data.name || file.replace(".collection.yml", ""),
|
||||
description: data.description || "",
|
||||
tags: data.tags || [],
|
||||
tags: tags,
|
||||
featured: data.featured || false,
|
||||
items: (data.items || []).map((item) => ({
|
||||
path: item.path,
|
||||
@@ -235,11 +418,18 @@ function generateCollectionsData() {
|
||||
}
|
||||
|
||||
// Sort with featured first, then alphabetically
|
||||
return collections.sort((a, b) => {
|
||||
const sortedCollections = collections.sort((a, b) => {
|
||||
if (a.featured && !b.featured) return -1;
|
||||
if (!a.featured && b.featured) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return {
|
||||
items: sortedCollections,
|
||||
filters: {
|
||||
tags: Array.from(allTags).sort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,20 +506,25 @@ async function main() {
|
||||
ensureDataDir();
|
||||
|
||||
// Generate all data
|
||||
const agents = generateAgentsData();
|
||||
console.log(`✓ Generated ${agents.length} agents`);
|
||||
const agentsData = generateAgentsData();
|
||||
const agents = agentsData.items;
|
||||
console.log(`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`);
|
||||
|
||||
const prompts = generatePromptsData();
|
||||
console.log(`✓ Generated ${prompts.length} prompts`);
|
||||
const promptsData = generatePromptsData();
|
||||
const prompts = promptsData.items;
|
||||
console.log(`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`);
|
||||
|
||||
const instructions = generateInstructionsData();
|
||||
console.log(`✓ Generated ${instructions.length} instructions`);
|
||||
const instructionsData = generateInstructionsData();
|
||||
const instructions = instructionsData.items;
|
||||
console.log(`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`);
|
||||
|
||||
const skills = generateSkillsData();
|
||||
console.log(`✓ Generated ${skills.length} skills`);
|
||||
const skillsData = generateSkillsData();
|
||||
const skills = skillsData.items;
|
||||
console.log(`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`);
|
||||
|
||||
const collections = generateCollectionsData();
|
||||
console.log(`✓ Generated ${collections.length} collections`);
|
||||
const collectionsData = generateCollectionsData();
|
||||
const collections = collectionsData.items;
|
||||
console.log(`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`);
|
||||
|
||||
const searchIndex = generateSearchIndex(agents, prompts, instructions, skills, collections);
|
||||
console.log(`✓ Generated search index with ${searchIndex.length} items`);
|
||||
@@ -337,27 +532,27 @@ async function main() {
|
||||
// Write JSON files
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "agents.json"),
|
||||
JSON.stringify(agents, null, 2)
|
||||
JSON.stringify(agentsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "prompts.json"),
|
||||
JSON.stringify(prompts, null, 2)
|
||||
JSON.stringify(promptsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "instructions.json"),
|
||||
JSON.stringify(instructions, null, 2)
|
||||
JSON.stringify(instructionsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "skills.json"),
|
||||
JSON.stringify(skills, null, 2)
|
||||
JSON.stringify(skillsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(WEBSITE_DATA_DIR, "collections.json"),
|
||||
JSON.stringify(collections, null, 2)
|
||||
JSON.stringify(collectionsData, null, 2)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
Reference in New Issue
Block a user