import { ArrowLeftIcon, ArrowRightIcon, ArrowUpRightIcon, CheckIcon, ChevronDownIcon, } from "@primer/octicons-react"; import { clsx } from "clsx"; import React, { useMemo, useState } from "react"; import { Box, Button, Checkbox, FormControl, Grid, Heading, Image, Pagination, Section, Stack, Text, useTheme, } from "@primer/react-brand"; import styles from "./styles/learning-hub-copilot-app.module.css"; import { DuckIcon } from "./DuckIcon"; import { LearningHubIcon } from "./LearningHubIcon"; import { PageShell } from "./PageShell"; import { ScrambleText } from "./ScrambleText"; import { pageHref } from "./pageHref"; import type { SearchItem } from "./searchIndex"; import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data"; type Topic = | "Getting started" | "Agents & Skills" | "Customization" | "Automation" | "Extensions & MCP" | "CLI"; type Kind = "Guide" | "Tutorial" | "Terminology" | "Example" | "Workshop"; /** Article frontmatter projected out of the `docs` collection at build time. */ export type LearningHubArticle = { /** Collection entry id, e.g. `learning-hub/agentic-workflows`. */ id: string; title: string; description: string; href: string; tags: string[]; /** ISO date string; the collection stores a Date, serialised across the island boundary. */ lastUpdated?: string; }; type Entry = LearningHubArticle & { topic: Topic; kind: Kind; isNew?: boolean; }; // The prototype hardcoded a topic/kind per article. Real frontmatter only // carries free-form `tags`, so the two facets are derived from them; the first // matching tag wins, in the order listed here. const TOPIC_BY_TAG: [string, Topic][] = [ ["cli", "CLI"], ["mcp", "Extensions & MCP"], ["canvases", "Extensions & MCP"], ["canvas-extensions", "Extensions & MCP"], ["extensions", "Extensions & MCP"], ["plugins", "Extensions & MCP"], ["agents", "Agents & Skills"], ["subagents", "Agents & Skills"], ["skills", "Agents & Skills"], ["orchestration", "Agents & Skills"], ["coding-agent", "Automation"], ["automation", "Automation"], ["automations", "Automation"], ["workflows", "Automation"], ["hooks", "Automation"], ["github-actions", "Automation"], ["customization", "Customization"], ["instructions", "Customization"], ["configuration", "Customization"], ]; const KIND_BY_TAG: [string, Kind][] = [ ["workshop", "Workshop"], ["glossary", "Terminology"], ["terminology", "Terminology"], ["reference", "Terminology"], ["examples", "Example"], ["tutorial", "Tutorial"], ["setup", "Tutorial"], ["desktop", "Tutorial"], ]; function deriveTopic(tags: string[]): Topic { for (const [tag, topic] of TOPIC_BY_TAG) { if (tags.includes(tag)) return topic; } return "Getting started"; } function deriveKind(tags: string[]): Kind { for (const [tag, kind] of KIND_BY_TAG) { if (tags.includes(tag)) return kind; } return "Guide"; } type FilterGroupId = "topic" | "kind"; // The prototype also offered a "Level" facet. No article frontmatter records a // level, so the group is omitted rather than populated with invented values. const filterGroups: { id: FilterGroupId; label: string; options: string[] }[] = [ { id: "topic", label: "Topic", options: [ "Getting started", "Agents & Skills", "Customization", "Automation", "Extensions & MCP", "CLI", ], }, { id: "kind", label: "Content type", options: ["Guide", "Tutorial", "Terminology", "Example", "Workshop"], }, ]; type FilterState = Record; const emptyFilters: FilterState = { topic: [], kind: [], }; const recommendedCards = [ { id: "desktop-app", page: "github-copilot-app", labels: [] as string[], title: "Desktop App", description: "Explore the GitHub Copilot app — a control center for directing multiple agents in parallel. Perfect for agent-native development and parallel work with isolated worktrees.", }, { id: "app-course", page: "learning-hub/app-for-beginners", labels: [] as string[], title: "Copilot app for Beginners", description: "Work through eight chapters on sessions, worktrees, context, development workflows, skills, MCP, canvases, and automations. Use the course sample to practice each step.", }, { id: "reference", page: "github-copilot-terminology-glossary", labels: [] as string[], title: "Terminology", description: "Quick-lookup resources to keep handy while you work. Browse the GitHub Copilot Terminology Glossary for definitions of common terms and concepts.", }, { id: "terminal", page: "cli-for-beginners", labels: [] as string[], title: "Copilot CLI", description: "Looking for a guided path into GitHub Copilot from the terminal? Explore the Copilot CLI for Beginners with a text-based experience or the YouTube video series.", }, { id: "fundamentals", page: "what-are-agents-skills-instructions", labels: [] as string[], title: "Fundamentals", description: "Essential concepts to tailor GitHub Copilot beyond its default experience. Start with What are Agents, Skills, and Instructions and work through the full track to master every customization primitive. For delegation and orchestration patterns, continue with Agents and Subagents.", }, { id: "automations", page: "using-automations-in-copilot-app", labels: [] as string[], title: "Automations", description: "Start with Using Automations in the GitHub Copilot app for templates, setup guidance, and real examples.", }, { id: "canvases", page: "working-with-canvas-extensions", labels: [] as string[], title: "Canvases", description: "Learn Working with Canvas Extensions to create and evolve interactive canvases with /create-canvas.", }, { id: "workshop", page: "copilot-workshops", labels: [] as string[], title: "Workshops", description: "Prefer to learn by building? Start with First Steps with GitHub Copilot to build and ship a Space Quiz in VS Code, Copilot CLI, or the Copilot app. Then explore Real-World Development with GitHub Copilot using a shared Tailspin Toys backlog.", }, ]; export function LearningHubIndex({ articles, searchIndex = [], contributorsTotal = siteContributorsTotal, }: { articles: LearningHubArticle[]; searchIndex?: SearchItem[]; contributorsTotal?: number; }) { return ( ); } function LearningHubIndexBody({ articles, searchIndex, }: { articles: LearningHubArticle[]; searchIndex: SearchItem[]; }) { const { colorMode } = useTheme(); const [currentPage, setCurrentPage] = useState(1); const [searchTerm, setSearchTerm] = useState(""); const [heroSearchFocused, setHeroSearchFocused] = useState(false); const [heroActiveIndex, setHeroActiveIndex] = useState(-1); const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); const heroSearchInputRef = React.useRef(null); const entries = useMemo(() => { const newest = articles.reduce((latest, article) => { if (!article.lastUpdated) return latest; return !latest || article.lastUpdated > latest ? article.lastUpdated : latest; }, undefined); return articles.map((article) => ({ ...article, topic: deriveTopic(article.tags), kind: deriveKind(article.tags), isNew: newest !== undefined && article.lastUpdated === newest, })); }, [articles]); React.useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if ( event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey ) { const target = event.target as HTMLElement | null; const isTyping = target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.isContentEditable === true; if (!isTyping) { event.preventDefault(); heroSearchInputRef.current?.focus(); } } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, []); const REC_PER_PAGE = 3; const recPageCount = Math.ceil(recommendedCards.length / REC_PER_PAGE); const [recPage, setRecPage] = React.useState(0); const changeRecPage = (direction: 1 | -1) => { setRecPage((page) => (page + direction + recPageCount) % recPageCount); }; const visibleRecCards = recommendedCards.slice( recPage * REC_PER_PAGE, recPage * REC_PER_PAGE + REC_PER_PAGE, ); const [filters, setFilters] = useState(emptyFilters); const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest"); const filteredEntries = useMemo(() => { const query = searchTerm.trim().toLowerCase(); return entries.filter((entry) => { const topicOk = filters.topic.length === 0 || filters.topic.includes(entry.topic); const kindOk = filters.kind.length === 0 || filters.kind.includes(entry.kind); const searchOk = query.length === 0 || [entry.title, entry.description, entry.topic, entry.kind, ...entry.tags] .join(" ") .toLowerCase() .includes(query); return topicOk && kindOk && searchOk; }); }, [entries, filters, searchTerm]); const sortedEntries = useMemo(() => { return sortOrder === "oldest" ? [...filteredEntries].reverse() : filteredEntries; }, [filteredEntries, sortOrder]); const articleMatches = useMemo(() => { const query = searchTerm.trim().toLowerCase(); if (query.length === 0) return []; return entries .filter( (entry) => entry.title.toLowerCase().includes(query) || entry.description.toLowerCase().includes(query), ) .slice(0, 5); }, [entries, searchTerm]); const resourceMatches = useMemo(() => { const query = searchTerm.trim().toLowerCase(); if (query.length === 0) return []; return searchIndex .filter( (resource) => resource.title.toLowerCase().includes(query) || resource.description.toLowerCase().includes(query) || resource.category.toLowerCase().includes(query), ) .slice(0, 5); }, [searchIndex, searchTerm]); const trimmedSearch = searchTerm.trim(); const heroSearchOpen = heroSearchFocused && trimmedSearch.length > 0; const hasSearchResults = articleMatches.length > 0 || resourceMatches.length > 0; const heroOptionCount = articleMatches.length + resourceMatches.length; // A fresh query re-orders the list, so drop any stale highlight. React.useEffect(() => { setHeroActiveIndex(-1); }, [trimmedSearch]); React.useEffect(() => { if (heroActiveIndex < 0) return; document .getElementById(`hero-search-option-${heroActiveIndex}`) ?.scrollIntoView({ block: "nearest" }); }, [heroActiveIndex]); const handleResultsKeyDown = ( event: React.KeyboardEvent, optionCount: number, activeIndex: number, setActiveIndex: React.Dispatch>, optionIdPrefix: string, ) => { if (optionCount === 0) return; switch (event.key) { case "ArrowDown": event.preventDefault(); setActiveIndex((i) => (i + 1) % optionCount); break; case "ArrowUp": event.preventDefault(); setActiveIndex((i) => (i <= 0 ? optionCount - 1 : i - 1)); break; case "Home": event.preventDefault(); setActiveIndex(0); break; case "End": event.preventDefault(); setActiveIndex(optionCount - 1); break; case "Enter": if (activeIndex >= 0) { event.preventDefault(); document.getElementById(`${optionIdPrefix}-${activeIndex}`)?.click(); } break; } }; const toggleFilter = (groupId: FilterGroupId, option: string) => { setFilters((prev) => { const current = prev[groupId]; const next = current.includes(option) ? current.filter((value) => value !== option) : [...current, option]; return { ...prev, [groupId]: next }; }); setCurrentPage(1); }; const clearFilters = () => { setFilters(emptyFilters); setCurrentPage(1); }; const activeFilterCount = filters.topic.length + filters.kind.length; const hasActiveFilters = activeFilterCount > 0; const pageSize = 6; const pageCount = Math.max(1, Math.ceil(sortedEntries.length / pageSize)); const page = Math.min(currentPage, pageCount); const visibleEntries = sortedEntries.slice( (page - 1) * pageSize, page * pageSize, ); return ( <>
GitHub Copilot Learning Hub
Get started
Articles
Sort by:
{sortOrder === "newest" ? "Newest" : "Oldest"}
{(["newest", "oldest"] as const).map((value) => ( ))}
{visibleEntries.map((entry) => (
{entry.isNew ? ( New ) : null} {entry.topic} {entry.kind}

{entry.title}

{entry.description}

))}
{filteredEntries.length === 0 ? ( No sections match the selected filters. ) : null}