diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9d1fa24..eaac3f1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,6 @@ name: Deploy to GitHub Pages on: push: - branches: [main] workflow_dispatch: permissions: diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 0d2ef0d..26fc67e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,8 +10,8 @@ permissions: contents: read jobs: - content: - name: Content and site + nodejs: + name: Node.js / TypeScript build runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -19,123 +19,13 @@ jobs: - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.11" - - - name: Validate lessons and site behavior - run: bash scripts/validate-workshop.sh content - - smoke-build: - name: ${{ matrix.name }} - runs-on: ubuntu-latest - env: - CARGO_TARGET_DIR: ${{ github.workspace }}/.cargo-target - strategy: - fail-fast: false - matrix: - include: - - language: dotnet - name: .NET smoke build - - language: nodejs - name: Node.js / TypeScript smoke build - - language: python - name: Python smoke build - - language: go - name: Go smoke build - - language: rust - name: Rust smoke build - - language: java - name: Java smoke build - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - if: matrix.language == 'dotnet' - with: - dotnet-version: 10.0.x - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - if: matrix.language == 'nodejs' - with: - node-version: 22 cache: npm cache-dependency-path: | - start-accessibility/nodejs/package-lock.json start-museum/nodejs/package-lock.json - finished/nodejs/*/package-lock.json + finished/nodejs/museum-exhibit-studio/package-lock.json - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - if: matrix.language == 'python' - with: - python-version: "3.11" - cache: pip - cache-dependency-path: | - start-accessibility/python/requirements.txt - start-museum/python/requirements.txt - finished/python/*/requirements.txt - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - if: matrix.language == 'go' - with: - go-version: "1.24.x" - cache-dependency-path: | - start-accessibility/go/go.sum - start-museum/go/go.sum - finished/go/*/go.sum - - - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - if: matrix.language == 'rust' - with: - toolchain: "1.94.0" - - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - if: matrix.language == 'rust' - with: - workspaces: | - start-accessibility/rust -> ../../.cargo-target - start-museum/rust -> ../../.cargo-target - finished/rust/accessibility-report -> ../../../.cargo-target - finished/rust/hello-copilot-sdk -> ../../../.cargo-target - finished/rust/museum-exhibit-studio -> ../../../.cargo-target - - - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - if: matrix.language == 'java' - with: - distribution: temurin - java-version: "17" - cache: maven - - - name: Verify Maven - if: matrix.language == 'java' - run: mvn --version - - - name: Build ${{ matrix.name }} - run: bash scripts/validate-workshop.sh "${{ matrix.language }}" - - macos-portability: - name: macOS portable validation - runs-on: macos-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 - with: - dotnet-version: 10.0.x - - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.11" - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version: "1.24.x" + - name: Build starter and finished app + run: bash scripts/validate-workshop.sh nodejs - - name: Validate with macOS system Bash - run: | - /bin/bash --version - /bin/bash scripts/validate-workshop.sh dotnet - /bin/bash scripts/validate-workshop.sh python - /bin/bash scripts/validate-workshop.sh go - test -z "$(git status --porcelain --untracked-files=all)" + - name: Builds leave the repository clean + run: test -z "$(git status --porcelain --untracked-files=all)" diff --git a/.gitignore b/.gitignore index 609f45d..749b391 100644 --- a/.gitignore +++ b/.gitignore @@ -1,50 +1,14 @@ -# .NET build outputs -bin/ -obj/ -out/ - # VS Code .vscode/ *.code-workspace -# User-specific files -*.user -*.suo -*.userprefs - -# NuGet -*.nupkg -packages/ - -# Test results -TestResults/ - # OS files .DS_Store Thumbs.db -# Temporary inspection projects -temp-inspect/ - -# Playwright MCP run artifacts -.playwright-mcp/ - -# Cross-language validation outputs +# Node.js node_modules/ dist/ -target/ -.cargo-target/ -__pycache__/ -*.pyc -copilot-sdk-workshop-go.exe -copilot-sdk-workshop-go -copilot-sdk-workshop-museum-starter.exe -copilot-sdk-workshop-museum-starter - -# In-place learner environments -.venv/ -venv/ # Generated workshop artifacts -accessibility-report.html exhibit.html diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 2029497..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,8 +0,0 @@ - - - $(COPILOT_CLI_BINARY_PATH) - $(LOCALAPPDATA)\Microsoft\WinGet\Links\copilot.exe - /opt/homebrew/bin/copilot - /usr/local/bin/copilot - - \ No newline at end of file diff --git a/README.md b/README.md index de9ef15..7540ec1 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,27 @@ -# GitHub Copilot SDK Workshops +# GitHub Copilot SDK Workshop: Museum Exhibit Studio Start today: http://github.github.com/copilot-sdk-workshop/ -Choose one of two hands-on GitHub Copilot SDK workshops in .NET, Node.js/TypeScript, Python, Go, -Rust, or Maven Java: +A hands-on GitHub Copilot SDK workshop in Node.js and TypeScript. You build **Museum Exhibit +Studio**, a non-SDLC curator agent that transforms educator-approved facts into visitor-ready exhibit +copy behind deterministic application boundaries. The site and lessons are available in English and +Brazilian Portuguese. -- **Accessibility Reviewer:** build an SDLC developer tool that inspects a web page, consults - application-owned WCAG guidance, and produces an evidence-based report. -- **Museum Exhibit Studio:** build a non-SDLC curator that transforms educator-approved facts into - visitor-ready exhibit copy behind deterministic application boundaries. - -Across the workshops, you'll: +In the workshop, you'll: 1. Create a Copilot client and conversation session. 2. Separate durable agent policy from task-specific data. -3. Choose between local tools, MCP tools, and a tightly scoped single-tool allowlist. +3. Choose between a local application-owned tool, an MCP server, and a tightly scoped allowlist. 4. Enforce capability, input, timeout, validation, and lifecycle boundaries in application code. 5. Explain what the model can infer and what the application must prove. -Plan on about 90 minutes for Accessibility Reviewer or 90 minutes for Museum Exhibit Studio. -Machine setup happens separately in an untimed preflight for each workshop. +Plan on about 90 minutes. Machine setup happens separately in an untimed preflight. ## Start the workshop -Open the GitHub Pages URL produced by the repository's **Deploy to GitHub Pages** workflow. Choose a -workshop outcome, choose a language, then start the selected workshop. The site derives its Pages -base URL at runtime, so there is no hardcoded organization or user Pages hostname. +Open the GitHub Pages URL produced by the repository's **Deploy to GitHub Pages** workflow, choose +the content language, and start the workshop. The site derives its Pages base URL at runtime, so +there is no hardcoded organization or user Pages hostname. To preview the site from a clone: @@ -40,39 +36,23 @@ the Markdown requests used by the lesson viewer. ## Prerequisites -- [.NET 10 SDK](https://learn.microsoft.com/dotnet/core/install/) -- [Node.js 22 or newer](https://nodejs.org/) -- [Python 3.11 or newer](https://www.python.org/downloads/) -- [Go 1.24 or newer](https://go.dev/dl/) -- [Rust 1.94 or newer](https://rustup.rs/) -- [Java 17 or newer](https://adoptium.net/) and [Maven](https://maven.apache.org/install.html) +- [Node.js 22.12 or newer](https://nodejs.org/) and npm - [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) - GitHub Copilot subscription or trial -- Microsoft Edge (the workshop default) or Google Chrome -Preflight walks through installation checks, authentication, OS-specific commands, expected -output, and troubleshooting. +Preflight walks through installation checks, authentication, expected output, and troubleshooting. ## Repository layout ```text copilot-sdk-workshop/ -|-- docs/ GitHub Pages site and controlled target page -|-- workshop/ Two complete workshop tracks and optional extensions -|-- start-accessibility/ Accessibility Reviewer starters in all six languages -|-- start-museum/ Museum Exhibit Studio starters in all six languages -|-- finished/dotnet/ -| |-- hello-copilot-sdk/ Completed local-tool example in every language -| |-- accessibility-report/ Completed .NET local + MCP reporter -| `-- museum-exhibit-studio/ Grounded museum curator sample, one application-owned tool -|-- finished/nodejs/ Completed TypeScript projects -|-- finished/python/ Completed Python projects -|-- finished/go/ Completed Go projects -|-- finished/rust/ Completed Rust projects -|-- finished/java/ Completed Maven Java projects -|-- src/BlazorApp/ Source counterpart of the deployed target -|-- scripts/ Deterministic content and build validation -`-- .github/workflows/ Validation and Pages deployment +|-- docs/ GitHub Pages site and lesson viewer +|-- workshop/ Lessons in English +| `-- pt-BR/ Lessons in Brazilian Portuguese +|-- start-museum/nodejs/ Starter project learners grow in place +|-- finished/nodejs/museum-exhibit-studio/ Completed application +|-- scripts/ Content, build, and deployment validation +`-- .github/workflows/ Validation and Pages deployment ``` ## Validate a change @@ -81,41 +61,39 @@ copilot-sdk-workshop/ bash scripts/validate-workshop.sh ``` -The command checks lesson structure, internal links, site behavior hooks, and project coverage. -It then runs browser-independent language-selection tests and restores, builds, or syntax-checks every -accessibility and museum starter, every finished project, and the Blazor target without authenticating -Copilot, launching a browser, or sending a prompt. The museum projects ship no tests, mocks, or -fixtures, so their targets only restore and build. +The command checks that every lesson listed in the lesson viewer exists in English and Brazilian +Portuguese, that translated lessons keep the same code blocks and links as the English ones, that +the starter helper module matches the finished app, and that the site navigation and localization +tests pass. It then installs and type-checks the starter and the finished app without +authenticating Copilot or sending a prompt. -Pass a language ID to run one smoke-build target: +Run one part at a time with `content` or `nodejs`: ```bash +bash scripts/validate-workshop.sh content bash scripts/validate-workshop.sh nodejs ``` -Pull requests run content validation and all six language smoke builds as separate GitHub Actions -jobs, so a failure identifies the affected SDK track. - ## Museum Exhibit Studio workshop -Museum Exhibit Studio starters live under `start-museum/`, with completed references under -`finished//museum-exhibit-studio`. Each starter ships one pre-built curator helper module -that learners never edit: approved fact sets and their bounds, a streaming printer, deterministic -exhibit validation, the scoped Wikipedia MCP server with its deny-by-default permission handler, the -single-file `exhibit.html` write permission, and small terminal prompts. +The starter lives under `start-museum/nodejs`, with the completed application under +`finished/nodejs/museum-exhibit-studio`. The starter ships one pre-built curator helper module, +`src/curator.ts`, that learners never edit: approved fact sets and their bounds, a streaming +function, deterministic exhibit validation, the scoped Wikipedia MCP server with its deny-by-default +permission handler, the single-file `exhibit.html` write permission, and small terminal prompts. -Learners work directly in `start-museum/` and grow that one project across the -lessons, running it at every step. They write only the session setup, the curator and research -system messages, the prompt builders, one session runner that owns the lifecycle and guardrails, and -`main`. The finished sample is what a learner ends up with, not a separate reference architecture. +Learners work directly in `start-museum/nodejs` and grow `src/index.ts` across the lessons, running +it at every step. They write only the session setup, the curator and research system messages, the +prompt builders, one session runner that owns the lifecycle and guardrails, and `main`. The finished +sample is what a learner ends up with, not a separate reference architecture. The learner-facing track begins at [`workshop/museum-00-preflight.md`](workshop/museum-00-preflight.md), then runs through seven core steps — first session, streaming, curator voice, approved facts, guardrails, structural checks, and -Wikipedia MCP research — plus an optional interactive `exhibit.html` capstone. - -Rust checks share one Cargo target directory across all workshop projects, avoiding repeated SDK -dependency compilation. +Wikipedia MCP research — plus an optional interactive `exhibit.html` capstone. Brazilian Portuguese +lessons live in [`workshop/pt-BR/`](workshop/pt-BR/); there, the prompts and user-facing messages +are shown in Portuguese, while the code structure and the `## Narrative`, `## Visitor questions`, and +`## Sources` headings that the helper module parses stay in English. ## Deployment @@ -137,16 +115,10 @@ WORKSHOP_SITE_URL=https://workshop.example.com/ python3 scripts/validate_deploym ## References -- [GitHub Copilot SDK for .NET](https://github.com/github/copilot-sdk/tree/main/dotnet) - [GitHub Copilot SDK for Node.js/TypeScript](https://github.com/github/copilot-sdk/tree/main/nodejs) -- [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk/tree/main/python) -- [GitHub Copilot SDK for Go](https://github.com/github/copilot-sdk/tree/main/go) -- [GitHub Copilot SDK for Rust](https://github.com/github/copilot-sdk/tree/main/rust) -- [GitHub Copilot SDK for Java](https://github.com/github/copilot-sdk/tree/main/java) - [Copilot SDK cookbook](https://github.com/github/copilot-sdk/tree/main/cookbook) - [Copilot SDK API and source](https://github.com/github/copilot-sdk) - [Install the GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) -- [Playwright MCP](https://github.com/microsoft/playwright-mcp) - [Model Context Protocol](https://modelcontextprotocol.io/) ## License diff --git a/docs/homepage.js b/docs/homepage.js index 5f5e40a..60839d1 100644 --- a/docs/homepage.js +++ b/docs/homepage.js @@ -1,166 +1,61 @@ (function () { 'use strict'; - const storageKey = 'copilot-sdk-workshop.language'; - const picker = document.getElementById('languagePicker'); - const languageInputs = [...document.querySelectorAll('input[name="language"]')]; + const localeStorageKey = 'copilot-sdk-workshop.locale'; + const localeSelector = document.getElementById('localeSelector'); const startLink = document.getElementById('startWorkshopLink'); - const docsLink = document.getElementById('sdkDocsLink'); - const summary = document.getElementById('languageSummary'); - const installCommand = document.getElementById('installCommand'); - const runtimeNote = document.getElementById('runtimeNote'); - const workshopInputs = [...document.querySelectorAll('input[name="workshop"]')]; - const targetAppLink = document.getElementById('targetAppLink'); - const previewTitle = document.getElementById('previewTitle'); - const preview = document.getElementById('workshopPreview'); - const startGuidance = document.getElementById('startGuidance'); - let selectedWorkshopId = null; - - const workshops = { - sdlc: { - name: 'Accessibility reviewer', - previewTitle: 'accessibility-reviewer', - preview: `URL → Playwright inspection - → WCAG lookup - → structured report - -[tool] playwright-browser_navigate -[tool] accessibility_rule_lookup - -Finding -The name input has no accessible name.`, - guidance: 'Build an SDLC developer tool in a 90-minute core workshop.' - }, - museum: { - name: 'Museum Exhibit Studio', - previewTitle: 'museum-exhibit-studio', - preview: `Approved facts → curator session - → exhibit validation - → visitor-ready copy - -Available tools: [] -System message: replace - -# Journey to the Moon -## Narrative -## Visitor questions`, - guidance: 'Build a non-SDLC curator tool in a 90-minute core workshop.' - } - }; + let locale = WorkshopLocalization.resolveLocale( + window.location.search, + getStoredValue(localeStorageKey) + ); - function getStoredLanguageId() { + function getStoredValue(key) { try { - return window.localStorage.getItem(storageKey); + return window.localStorage.getItem(key); } catch (error) { return null; } } - function storeLanguageId(languageId) { + function storeValue(key, value) { try { - window.localStorage.setItem(storageKey, languageId); + window.localStorage.setItem(key, value); } catch (error) { // Local storage can be unavailable in private browsing contexts. } } - function updateSelection(languageId) { - const language = WorkshopLanguages.getLanguage(languageId); - const hasLanguage = language !== null; - const workshop = selectedWorkshopId ? workshops[selectedWorkshopId] : null; - const ready = workshop !== null && hasLanguage; - - picker.disabled = workshop === null; - document.querySelectorAll('.language-option').forEach(option => { - option.classList.toggle('selected', option.dataset.language === language?.id); + function applyLocale() { + const messages = WorkshopLocalization.getMessages(locale.id); + document.documentElement.lang = locale.id; + document.title = messages.pageTitle; + document.querySelector('meta[name="description"]').content = messages.pageDescription; + localeSelector.value = locale.id; + document.querySelectorAll('[data-i18n]').forEach(element => { + element.textContent = messages[element.dataset.i18n]; }); - startLink.classList.toggle('disabled', !ready); - startLink.setAttribute('aria-disabled', String(!ready)); - startLink.href = ready - ? WorkshopLanguageNavigation.firstLessonUrl(language.id, selectedWorkshopId) - : workshop ? '#language-picker' : '#workshop-picker'; - startLink.textContent = ready ? `Start ${workshop.name}` : 'Start selected workshop'; - targetAppLink.hidden = selectedWorkshopId !== 'sdlc'; - - if (!hasLanguage) { - docsLink.removeAttribute('href'); - docsLink.setAttribute('aria-disabled', 'true'); - summary.textContent = workshop - ? `Now choose a language for ${workshop.name}.` - : 'Choose a workshop first, then select its implementation language.'; - installCommand.textContent = ''; - runtimeNote.textContent = ''; - startGuidance.textContent = workshop?.guidance ?? - 'Choose a workshop and language. No prior agent or SDK experience required.'; - return; - } - - docsLink.href = language.docsUrl; - docsLink.removeAttribute('aria-disabled'); - docsLink.textContent = `${language.displayName} SDK docs ↗`; - summary.textContent = workshop - ? `${workshop.name} will use the ${language.displayName} SDK.` - : 'Choose a workshop to continue.'; - installCommand.textContent = language.installCommand; - runtimeNote.textContent = language.runtimeNote; - startGuidance.textContent = workshop?.guidance ?? 'Choose a workshop to continue.'; - } - - function selectWorkshop(workshopId) { - selectedWorkshopId = workshops[workshopId] ? workshopId : null; - document.querySelectorAll('.workshop-option').forEach(option => { - option.classList.toggle('selected', option.dataset.workshop === selectedWorkshopId); + document.querySelectorAll('[data-i18n-aria-label]').forEach(element => { + element.setAttribute('aria-label', messages[element.dataset.i18nAriaLabel]); }); - const workshop = selectedWorkshopId ? workshops[selectedWorkshopId] : null; - previewTitle.textContent = workshop?.previewTitle ?? 'workshop-preview'; - preview.textContent = workshop?.preview ?? 'Select a workshop to preview its agent flow.'; - updateSelection(languageInputs.find(input => input.checked)?.value ?? null); - if (workshop) { - languageInputs[0].focus(); - } - } - - languageInputs.forEach(input => { - input.addEventListener('change', () => { - const language = WorkshopLanguages.getLanguage(input.value); - storeLanguageId(language.id); - updateSelection(language.id); + document.querySelectorAll('.theme-toggle').forEach(button => { + button.dataset.lightLabel = messages.themeLight; + button.dataset.darkLabel = messages.themeDark; + button.dataset.switchToLight = messages.switchToLight; + button.dataset.switchToDark = messages.switchToDark; }); - }); + updateToggleIcon(); + startLink.href = WorkshopNavigation.firstLessonUrl(locale.id); + } - workshopInputs.forEach(input => { - input.addEventListener('change', () => selectWorkshop(input.value)); + localeSelector.addEventListener('change', () => { + locale = WorkshopLocalization.getLocale(localeSelector.value); + storeValue(localeStorageKey, locale.id); + const url = new URL(window.location.href); + url.searchParams.set('locale', locale.id); + window.history.replaceState({}, '', url); + applyLocale(); }); - startLink.addEventListener('click', event => { - if (startLink.getAttribute('aria-disabled') === 'true') { - event.preventDefault(); - if (!selectedWorkshopId) { - workshopInputs[0].focus(); - } else { - languageInputs[0].focus(); - languageInputs[0].reportValidity(); - } - } - }); - - const initialLanguage = WorkshopLanguageNavigation.resolveLanguage( - window.location.search, - getStoredLanguageId(), - WorkshopLanguages.getLanguage - ); - if (initialLanguage) { - const matchingLanguage = languageInputs.find(input => input.value === initialLanguage.id); - matchingLanguage.checked = true; - storeLanguageId(initialLanguage.id); - } - - const requestedWorkshop = new URLSearchParams(window.location.search).get('workshop'); - const matchingWorkshop = workshopInputs.find(input => input.value === requestedWorkshop); - if (matchingWorkshop) { - matchingWorkshop.checked = true; - selectWorkshop(matchingWorkshop.value); - } else { - updateSelection(initialLanguage?.id ?? null); - } + applyLocale(); + storeValue(localeStorageKey, locale.id); }()); diff --git a/docs/index.html b/docs/index.html index 46fb7db..cbba6f6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,30 +3,36 @@ - - Choose your workshop | GitHub Copilot SDK + + Museum Exhibit Studio | GitHub Copilot SDK Workshop - - + + - + @@ -34,91 +40,50 @@
-

Choose what your agent is here to do.

-

- Two workshops. One SDK. Build an agent for the software lifecycle, or take - Copilot into a completely different domain. +

Build a museum curator agent with the GitHub Copilot SDK.

+

+ One hands-on workshop in Node.js and TypeScript. Take Copilot outside the software + lifecycle and turn approved facts into visitor-ready exhibit copy, with the guardrails in + your own code.

-
- Choose a workshop -
- -
-

Choose your workshop language

-

Choose a workshop first, then select its implementation language.

-
- Programming language - - - - - - -
-

+
+

npm install @github/copilot-sdkRequires Node.js 22.12 or newer.

+
- Start selected workshop -

Choose a workshop and language. No prior agent or SDK experience required.

+ Start the workshop +

No prior agent or SDK experience required. Machine setup happens in an untimed preflight.

-
diff --git a/docs/language-navigation.js b/docs/language-navigation.js deleted file mode 100644 index a123920..0000000 --- a/docs/language-navigation.js +++ /dev/null @@ -1,50 +0,0 @@ -(function (root, factory) { - const api = factory(); - if (typeof module === 'object' && module.exports) { - module.exports = api; - } - root.WorkshopLanguageNavigation = api; -}(typeof globalThis !== 'undefined' ? globalThis : this, function () { - 'use strict'; - - function resolveLanguage(search, storedLanguageId, getLanguage) { - const parameters = new URLSearchParams(search); - if (parameters.has('lang')) { - return getLanguage(parameters.get('lang')); - } - return getLanguage(storedLanguageId); - } - - function lessonUrl(stepId, languageId) { - const parameters = new URLSearchParams({ step: stepId }); - if (languageId) { - parameters.set('lang', languageId); - } - return `?${parameters.toString()}`; - } - - function homeUrl(languageId, workshopId) { - const parameters = new URLSearchParams(); - if (languageId) { - parameters.set('lang', languageId); - } - if (workshopId) { - parameters.set('workshop', workshopId); - } - const query = parameters.toString(); - return query ? `../index.html?${query}` : '../index.html'; - } - - function firstLessonUrl(languageId, workshopId = 'sdlc') { - const firstStep = workshopId === 'museum' - ? 'museum-00-preflight' - : '00-preflight'; - return `workshop/step.html${lessonUrl(firstStep, languageId)}`; - } - - function siteRootUrl(lessonPageUrl) { - return new URL('../', lessonPageUrl); - } - - return Object.freeze({ resolveLanguage, lessonUrl, homeUrl, firstLessonUrl, siteRootUrl }); -})); diff --git a/docs/language-registry.js b/docs/language-registry.js deleted file mode 100644 index cc8a779..0000000 --- a/docs/language-registry.js +++ /dev/null @@ -1,64 +0,0 @@ -(function (root, factory) { - const api = factory(); - if (typeof module === 'object' && module.exports) { - module.exports = api; - } - root.WorkshopLanguages = api; -}(typeof globalThis !== 'undefined' ? globalThis : this, function () { - 'use strict'; - - const languages = Object.freeze([ - { - id: 'dotnet', - displayName: '.NET', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/dotnet', - installCommand: 'dotnet add package GitHub.Copilot.SDK', - runtimeNote: 'Requires the .NET SDK and a supported C# runtime.' - }, - { - id: 'go', - displayName: 'Go', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/go', - installCommand: 'go get github.com/github/copilot-sdk/go', - runtimeNote: 'Requires a supported Go toolchain and module.' - }, - { - id: 'java', - displayName: 'Java', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/java', - installCommand: 'mvn dependency:get -Dartifact=com.github:copilot-sdk-java:1.0.11', - runtimeNote: 'Requires a supported JDK and a Maven or Gradle project.' - }, - { - id: 'nodejs', - displayName: 'Node.js', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/nodejs', - installCommand: 'npm install @github/copilot-sdk', - runtimeNote: 'Requires a current Node.js LTS release.' - }, - { - id: 'python', - displayName: 'Python', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/python', - installCommand: 'pip install github-copilot-sdk', - runtimeNote: 'Requires Python and an isolated virtual environment.' - }, - { - id: 'rust', - displayName: 'Rust', - docsUrl: 'https://github.com/github/copilot-sdk/tree/main/rust', - installCommand: 'cargo add copilot-sdk', - runtimeNote: 'Requires Rust and Cargo from rustup.' - } - ]); - - const languageById = Object.freeze(Object.fromEntries( - languages.map(language => [language.id, language]) - )); - - function getLanguage(languageId) { - return languageById[languageId] ?? null; - } - - return Object.freeze({ languages, getLanguage }); -})); diff --git a/docs/localization.js b/docs/localization.js new file mode 100644 index 0000000..3fd06dd --- /dev/null +++ b/docs/localization.js @@ -0,0 +1,199 @@ +(function (root, factory) { + const api = factory(); + if (typeof module === 'object' && module.exports) { + module.exports = api; + } + root.WorkshopLocalization = api; +}(typeof globalThis !== 'undefined' ? globalThis : this, function () { + 'use strict'; + + const defaultLocale = 'en'; + const locales = Object.freeze([ + Object.freeze({ id: 'en', displayName: 'English' }), + Object.freeze({ id: 'pt-BR', displayName: 'Português (Brasil)' }) + ]); + + const messages = Object.freeze({ + en: Object.freeze({ + pageTitle: 'Museum Exhibit Studio | GitHub Copilot SDK Workshop', + pageDescription: 'A hands-on GitHub Copilot SDK workshop in Node.js: build a grounded museum curator agent.', + skipLink: 'Skip to workshop overview', + homeLabel: 'GitHub Copilot SDK Workshop home', + resourcesLabel: 'Workshop resources', + sdkDocs: '📚 Node.js SDK docs ↗', + localeLabel: 'Content language', + themeLight: 'Light', + themeDark: 'Dark', + switchToLight: 'Switch to light theme', + switchToDark: 'Switch to dark theme', + heroTitle: 'Build a museum curator agent with the GitHub Copilot SDK.', + heroDefinition: 'One hands-on workshop in Node.js and TypeScript. Take Copilot outside the software lifecycle and turn approved facts into visitor-ready exhibit copy, with the guardrails in your own code.', + workshopLabel: 'The workshop', + museumKicker: 'Node.js · TypeScript · 90 minutes', + museumTitle: 'Curate a museum exhibit', + museumDescription: 'Build a grounded interpretive agent that turns approved facts into visitor-ready exhibit copy.', + museumCapabilities: 'Custom persona · one application-owned tool · MCP · validation', + runtimeNote: 'Requires Node.js 22.12 or newer.', + startWorkshop: 'Start the workshop', + startGuidance: 'No prior agent or SDK experience required. Machine setup happens in an untimed preflight.', + previewLabel: 'Workshop preview', + preview: `Approved facts → curator session + → exhibit validation + → visitor-ready copy + +Available tools: [] +System message: replace + +# Journey to the Moon +## Narrative +## Visitor questions`, + outcomesTitle: 'Build the app. Understand the boundary.', + outcomeSession: 'Create and manage a Copilot session.', + outcomePolicy: 'Separate durable agent policy from task data.', + outcomeTools: 'Choose the right tool surface for the job.', + outcomeValidation: 'Validate objective output requirements in code.', + outcomeControls: 'Explain where prompt guidance ends and hard controls begin.', + lesson: Object.freeze({ + pageDescription: 'A self-guided workshop for building a museum curator agent with the GitHub Copilot SDK for Node.js.', + skipLink: 'Skip to lesson', + openSections: 'Open sections', + closeSections: 'Close sections', + homeLabel: 'Copilot SDK Workshop home', + actionsLabel: 'Lesson actions', + hub: '🏠 Hub', + docs: '📚 Node.js docs ↗', + previous: 'Previous', + next: 'Next', + loading: 'Loading', + loadingLesson: 'Loading lesson.', + loadingLessonEllipsis: 'Loading lesson...', + progressLabel: 'Core workshop progress', + workshopSteps: 'Workshop steps', + paginationLabel: 'Lesson pagination', + beforeBegin: 'Before you begin', + coreWorkshop: 'Core workshop', + optionalExtension: 'Optional extension', + preflight: 'Preflight', + stepOf: 'Step {number} of {count}', + copy: 'Copy', + copied: 'Copied', + copyFailed: 'Copy failed', + copyCodeBlock: 'Copy code block', + unableToLoad: 'Unable to load this lesson', + serveLocally: 'Serve the repository with a local HTTP server; browsers block lesson fetches from file URLs.', + refreshGuidance: 'Refresh the page. If the problem continues, verify that the workshop Markdown was deployed.', + unableStatus: 'Unable to load this lesson.', + lessonRequestFailed: 'Lesson request failed with HTTP {status}.', + loadedStatus: 'Loaded {title}.', + loadingStatus: 'Loading {title}.', + fallbackNotice: 'This lesson is not available in Portuguese yet. Showing the English version.' + }), + steps: Object.freeze({}) + }), + 'pt-BR': Object.freeze({ + pageTitle: 'Museum Exhibit Studio | GitHub Copilot SDK Workshop', + pageDescription: 'Um workshop prático do GitHub Copilot SDK em Node.js: crie um agente curador de museu fundamentado em fatos.', + skipLink: 'Pular para a visão geral do workshop', + homeLabel: 'Página inicial do GitHub Copilot SDK Workshop', + resourcesLabel: 'Recursos do workshop', + sdkDocs: '📚 Documentação do SDK para Node.js ↗', + localeLabel: 'Idioma do conteúdo', + themeLight: 'Claro', + themeDark: 'Escuro', + switchToLight: 'Mudar para o tema claro', + switchToDark: 'Mudar para o tema escuro', + heroTitle: 'Crie um agente curador de museu com o GitHub Copilot SDK.', + heroDefinition: 'Um workshop prático em Node.js e TypeScript. Leve o Copilot para fora do ciclo de vida de software e transforme fatos aprovados em textos de exposição prontos para o público, com os guardrails no seu próprio código.', + workshopLabel: 'O workshop', + museumKicker: 'Node.js · TypeScript · 90 minutos', + museumTitle: 'Faça a curadoria de uma exposição de museu', + museumDescription: 'Crie um agente de curadoria baseado em fatos aprovados para produzir textos de exposição claros e adequados ao público.', + museumCapabilities: 'Persona personalizada · uma ferramenta controlada pelo aplicativo · MCP · validação', + runtimeNote: 'Requer Node.js 22.12 ou mais recente.', + startWorkshop: 'Começar o workshop', + startGuidance: 'Não é necessário ter experiência prévia com agentes ou com o SDK. A preparação da máquina é feita em uma etapa inicial sem tempo definido.', + previewLabel: 'Prévia do workshop', + preview: `Fatos aprovados → sessão do curador + → validação da exposição + → texto pronto para visitantes + +Ferramentas disponíveis: [] +Mensagem de sistema: replace + +# Viagem à Lua +## Narrativa +## Perguntas dos visitantes`, + outcomesTitle: 'Crie o aplicativo. Entenda os limites.', + outcomeSession: 'Crie e gerencie uma sessão do Copilot.', + outcomePolicy: 'Separe a política permanente do agente dos dados da tarefa.', + outcomeTools: 'Escolha o conjunto de ferramentas certo para cada tarefa.', + outcomeValidation: 'Valide em código os requisitos objetivos da saída.', + outcomeControls: 'Explique onde termina a orientação do prompt e onde começam os controles efetivos.', + lesson: Object.freeze({ + pageDescription: 'Um workshop autoguiado para criar um agente curador de museu com o GitHub Copilot SDK para Node.js.', + skipLink: 'Pular para a lição', + openSections: 'Abrir seções', + closeSections: 'Fechar seções', + homeLabel: 'Página inicial do Copilot SDK Workshop', + actionsLabel: 'Ações da lição', + hub: '🏠 Início', + docs: '📚 Documentação do Node.js ↗', + previous: 'Anterior', + next: 'Próxima', + loading: 'Carregando', + loadingLesson: 'Carregando lição.', + loadingLessonEllipsis: 'Carregando lição...', + progressLabel: 'Progresso do workshop principal', + workshopSteps: 'Etapas do workshop', + paginationLabel: 'Paginação das lições', + beforeBegin: 'Antes de começar', + coreWorkshop: 'Workshop principal', + optionalExtension: 'Extensão opcional', + preflight: 'Preparação', + stepOf: 'Etapa {number} de {count}', + copy: 'Copiar', + copied: 'Copiado', + copyFailed: 'Falha ao copiar', + copyCodeBlock: 'Copiar bloco de código', + unableToLoad: 'Não foi possível carregar esta lição', + serveLocally: 'Sirva o repositório com um servidor HTTP local. Os navegadores bloqueiam solicitações de lições em URLs file://.', + refreshGuidance: 'Atualize a página. Se o problema continuar, verifique se o Markdown do workshop foi publicado.', + unableStatus: 'Não foi possível carregar esta lição.', + lessonRequestFailed: 'A solicitação da lição falhou com o status HTTP {status}.', + loadedStatus: 'Lição carregada: {title}.', + loadingStatus: 'Carregando a lição: {title}.', + fallbackNotice: 'Esta lição ainda não está disponível em português. A versão em inglês será exibida.' + }), + steps: Object.freeze({ + 'museum-00-preflight': Object.freeze({ title: 'Prepare o Museum Exhibit Studio', navTitle: 'Preparação', time: 'Sem tempo definido' }), + 'museum-01-first-curator-session': Object.freeze({ title: 'Crie sua primeira sessão do curador', navTitle: 'Primeira sessão' }), + 'museum-02-stream-the-curator': Object.freeze({ title: 'Exiba a resposta do curador em streaming', navTitle: 'Streaming' }), + 'museum-03-curator-voice': Object.freeze({ title: 'Dê voz ao curador', navTitle: 'Voz do curador' }), + 'museum-04-approved-facts': Object.freeze({ title: 'Fundamente o texto em fatos aprovados', navTitle: 'Fatos aprovados' }), + 'museum-05-guardrails': Object.freeze({ title: 'Defina os guardrails', navTitle: 'Guardrails' }), + 'museum-06-prove-the-structure': Object.freeze({ title: 'Comprove a estrutura', navTitle: 'Verificações estruturais' }), + 'museum-07-wikipedia-research': Object.freeze({ title: 'Pesquise com o MCP da Wikipedia', navTitle: 'Pesquisa na Wikipedia' }), + 'museum-08-interactive-exhibit-page': Object.freeze({ title: 'Publique uma página interativa da exposição', navTitle: 'Página da exposição' }) + }) + }) + }); + + function getLocale(localeId) { + return locales.find(locale => locale.id.toLowerCase() === localeId?.toLowerCase()) ?? null; + } + + function resolveLocale(search, storedLocaleId) { + const requestedLocale = new URLSearchParams(search).get('locale'); + return getLocale(requestedLocale) ?? getLocale(storedLocaleId) ?? getLocale(defaultLocale); + } + + function getMessages(localeId) { + return messages[getLocale(localeId)?.id ?? defaultLocale]; + } + + function format(message, values = {}) { + return message.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match); + } + + return Object.freeze({ defaultLocale, format, getLocale, getMessages, locales, resolveLocale }); +})); \ No newline at end of file diff --git a/docs/markdown-language-preprocessor.js b/docs/markdown-language-preprocessor.js deleted file mode 100644 index 4f1f915..0000000 --- a/docs/markdown-language-preprocessor.js +++ /dev/null @@ -1,68 +0,0 @@ -(function (root, factory) { - const api = factory(); - if (typeof module === 'object' && module.exports) { - module.exports = api; - } - root.WorkshopMarkdown = api; -}(typeof globalThis !== 'undefined' ? globalThis : this, function () { - 'use strict'; - - function directiveError(lineNumber, message) { - return new Error(`Language directive error on line ${lineNumber}: ${message}`); - } - - function preprocessLanguageDirectives(markdown, languageId, getLanguage) { - if (typeof markdown !== 'string') { - throw new TypeError('Markdown must be a string.'); - } - if (typeof getLanguage !== 'function' || !getLanguage(languageId)) { - throw new Error(`A valid workshop language is required to render this lesson: "${languageId ?? ''}".`); - } - - const output = []; - let activeLanguageId = null; - const lines = markdown.split(/\r?\n/); - - lines.forEach((line, index) => { - const lineNumber = index + 1; - const languageMatch = line.match(/^:::language\s+(\S+)\s*$/); - const closingMatch = /^:::\s*$/.test(line); - const directiveLike = line.startsWith(':::'); - - if (languageMatch) { - if (activeLanguageId !== null) { - throw directiveError(lineNumber, 'language blocks cannot be nested.'); - } - if (!getLanguage(languageMatch[1])) { - throw directiveError(lineNumber, `unknown language "${languageMatch[1]}".`); - } - activeLanguageId = languageMatch[1]; - return; - } - - if (closingMatch) { - if (activeLanguageId === null) { - throw directiveError(lineNumber, 'closing directive has no open language block.'); - } - activeLanguageId = null; - return; - } - - if (directiveLike) { - throw directiveError(lineNumber, 'expected :::language or :::.'); - } - - if (activeLanguageId === null || activeLanguageId === languageId) { - output.push(line); - } - }); - - if (activeLanguageId !== null) { - throw directiveError(lines.length, `language block for "${activeLanguageId}" is not closed.`); - } - - return output.join('\n'); - } - - return Object.freeze({ preprocessLanguageDirectives }); -})); diff --git a/docs/navigation.js b/docs/navigation.js new file mode 100644 index 0000000..9d61769 --- /dev/null +++ b/docs/navigation.js @@ -0,0 +1,40 @@ +(function (root, factory) { + const api = factory(); + if (typeof module === 'object' && module.exports) { + module.exports = api; + } + root.WorkshopNavigation = api; +}(typeof globalThis !== 'undefined' ? globalThis : this, function () { + 'use strict'; + + const sdk = Object.freeze({ + displayName: 'Node.js', + docsUrl: 'https://github.com/github/copilot-sdk/tree/main/nodejs', + installCommand: 'npm install @github/copilot-sdk' + }); + const firstStepId = 'museum-00-preflight'; + + function lessonUrl(stepId, locale) { + const parameters = new URLSearchParams({ step: stepId }); + if (locale) { + parameters.set('locale', locale); + } + return `?${parameters.toString()}`; + } + + function homeUrl(locale) { + return locale + ? `../index.html?${new URLSearchParams({ locale }).toString()}` + : '../index.html'; + } + + function firstLessonUrl(locale) { + return `workshop/step.html${lessonUrl(firstStepId, locale)}`; + } + + function siteRootUrl(lessonPageUrl) { + return new URL('../', lessonPageUrl); + } + + return Object.freeze({ sdk, firstStepId, lessonUrl, homeUrl, firstLessonUrl, siteRootUrl }); +})); diff --git a/docs/step.css b/docs/step.css index f1e83e8..13b90f2 100644 --- a/docs/step.css +++ b/docs/step.css @@ -841,12 +841,6 @@ button { gap: 0.5rem; } -.language-select-label { - color: var(--text-secondary); - font-size: 0.75rem; - font-weight: 600; -} - .language-select { max-width: 8.5rem; padding: 0.42rem 0.5rem; @@ -858,6 +852,20 @@ button { font-size: 0.8rem; } +.locale-select { + max-width: 10.5rem; +} + +.translation-notice { + margin-bottom: 1rem; + padding: 0.75rem 1rem; + border: 1px solid var(--warning-yellow); + border-radius: 6px; + background: color-mix(in srgb, var(--warning-yellow) 10%, var(--bg-card)); + color: var(--text-primary); + font-size: 0.875rem; +} + .nav-btn, .header .theme-toggle { display: inline-flex; @@ -1182,7 +1190,6 @@ button { .header-title, .header-nav > a[href="../index.html"], #headerDocsLink, - .language-select-label, #previousHeaderLink { display: none !important; } @@ -1191,6 +1198,10 @@ button { max-width: 6.5rem; } + .locale-select { + max-width: 7.5rem; + } + .header-nav { margin-left: auto; gap: 0.3rem; diff --git a/docs/styles.css b/docs/styles.css index cacdc35..5cf8779 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -64,6 +64,18 @@ a { outline-offset: 3px; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + .skip-link { position: fixed; top: 0.75rem; @@ -147,6 +159,18 @@ a { border-radius: 6px; } +.locale-control select { + max-width: 11rem; + padding: 0.45rem 1.75rem 0.45rem 0.65rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font: inherit; + font-size: 0.86rem; + font-weight: 600; +} + .home-shell { width: min(1180px, calc(100% - 2.5rem)); min-height: calc(100svh - 4.5rem); @@ -208,42 +232,17 @@ h1 { border: 0; } -.workshop-picker legend { - margin-bottom: 0.55rem; - color: var(--muted); - font-family: var(--font-mono); - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; -} - .workshop-option { display: grid; - grid-template-columns: auto 1fr; - gap: 0.85rem; padding: 1rem 0; border-top: 1px solid var(--border); - cursor: pointer; - transition: border-color 150ms ease, color 150ms ease; -} - -.workshop-option:last-of-type { border-bottom: 1px solid var(--border); } -.workshop-option:hover, .workshop-option.selected { border-color: var(--neon-cyan); } -.workshop-option input { - width: 1.1rem; - height: 1.1rem; - margin: 0.28rem 0 0; - accent-color: var(--neon-cyan); -} - .workshop-option-copy { display: grid; gap: 0.25rem; @@ -294,82 +293,12 @@ h1 { transform: translateY(-2px); } -.primary-action.disabled, -.primary-action[aria-disabled="true"] { - cursor: not-allowed; - opacity: 0.55; - transform: none; -} - -.language-picker { - max-width: 720px; - margin: 1.55rem 0; - padding: 1rem 1.1rem; - border: 1px solid var(--border); - border-radius: 0.75rem; - background: var(--surface-soft); -} - -.language-picker h2 { - margin: 0 0 0.35rem; - font-size: 1rem; -} - -.language-picker p { - margin-bottom: 0.7rem; - color: var(--muted); - font-size: 0.9rem; -} - -.language-options { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.55rem; - margin: 0; - padding: 0; - border: 0; -} - -.language-options:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.language-option { - display: grid; - grid-template-columns: 1fr auto; - gap: 0.65rem; - align-items: center; - min-height: 3.4rem; - padding: 0.55rem 0.65rem; - border: 1px solid var(--border); - border-radius: 0.5rem; - background: var(--bg); - cursor: pointer; - font-size: 0.88rem; - font-weight: 700; - transition: border-color 150ms ease, background-color 150ms ease; -} - -.language-option:hover, -.language-option.selected { - border-color: var(--neon-cyan); - background: rgba(0, 245, 255, 0.07); -} - -.language-option input { - grid-column: 2; - grid-row: 1; - width: 1rem; - height: 1rem; - margin: 0; - accent-color: var(--neon-cyan); -} - -.language-picker .language-detail { +.language-detail { display: grid; gap: 0.35rem; - margin: 0.8rem 0 0; + margin: 0.9rem 0 1.55rem; + color: var(--muted); + font-size: 0.9rem; } .language-detail code:empty, @@ -531,6 +460,10 @@ h1 { gap: 0.5rem; } + .locale-control select { + max-width: 8.5rem; + } + .site-nav a { display: none; } @@ -544,10 +477,6 @@ h1 { flex-direction: column; } - .language-options { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .outcomes { grid-template-columns: 1fr; } diff --git a/docs/target-app/app.css b/docs/target-app/app.css deleted file mode 100644 index 1014592..0000000 --- a/docs/target-app/app.css +++ /dev/null @@ -1,90 +0,0 @@ -html, body { - margin: 0; - padding: 0; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background-color: #f8f9fa; - color: #212529; -} - -.page { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.banner { - background-color: #343a40; - color: #fff; - padding: 1rem 2rem; -} - -.banner h2 { - margin: 0; - font-size: 1.25rem; -} - -.content { - flex: 1; - padding: 2rem; - max-width: 800px; - margin: 0 auto; -} - -.hero { - background: #fff; - border-radius: 8px; - padding: 2rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.hero h2 { - margin-top: 0; -} - -.muted { - color: #adb5bd; -} - -.hero img { - display: block; - margin: 1.5rem 0; - border-radius: 4px; -} - -form { - display: flex; - gap: 0.5rem; - margin: 1rem 0; -} - -input[type="text"] { - padding: 0.5rem; - border: 1px solid #ced4da; - border-radius: 4px; - flex: 1; -} - -button { - padding: 0.5rem 1rem; - background-color: #0d6efd; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -button:focus { - outline: none; -} - -.low-contrast-link { - color: #d0d0d0; - text-decoration: none; -} - -.footer { - background-color: #e9ecef; - padding: 1rem 2rem; - text-align: center; - color: #6c757d; -} diff --git a/docs/target-app/index.html b/docs/target-app/index.html deleted file mode 100644 index 693a469..0000000 --- a/docs/target-app/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Blazor Accessibility Target - - - -
- - -
-
-

Welcome to the Accessibility Report Target

-

This page intentionally contains accessibility issues for the workshop.

- - - -

Quick actions

-
- - -
- -

About this app

-

- The accessibility report generator uses the GitHub Copilot SDK and the Playwright MCP server - to analyze this page and produce a WCAG-aligned report. -

- - Learn more -
-
- -
- Built for the Copilot SDK Workshop -
-
- - diff --git a/docs/tests/markdown-language-preprocessor.test.js b/docs/tests/markdown-language-preprocessor.test.js deleted file mode 100644 index b892356..0000000 --- a/docs/tests/markdown-language-preprocessor.test.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const assert = require('node:assert/strict'); -const { getLanguage } = require('../language-registry.js'); -const { - firstLessonUrl, - homeUrl, - lessonUrl, - resolveLanguage, - siteRootUrl -} = require('../language-navigation.js'); -const { preprocessLanguageDirectives } = require('../markdown-language-preprocessor.js'); - -const preprocess = (markdown, languageId) => - preprocessLanguageDirectives(markdown, languageId, getLanguage); - -assert.equal( - preprocess('Shared\n:::language dotnet\n.NET only\n:::\n:::language python\nPython only\n:::\nEnd', 'dotnet'), - 'Shared\n.NET only\nEnd' -); -assert.equal( - preprocess('Shared\n:::language dotnet\n.NET only\n:::\n:::language python\nPython only\n:::\nEnd', 'python'), - 'Shared\nPython only\nEnd' -); -assert.throws( - () => preprocess(':::language dotnet\n:::language python\n:::\n:::', 'dotnet'), - /cannot be nested/ -); -assert.throws(() => preprocess(':::language unknown\n:::', 'dotnet'), /unknown language/); -assert.throws(() => preprocess(':::language dotnet\nUnclosed', 'dotnet'), /not closed/); -assert.throws(() => preprocess(':::', 'dotnet'), /closing directive has no open/); -assert.throws(() => preprocess(':::unexpected', 'dotnet'), /expected :::language/); - -assert.equal(lessonUrl('04-mcp-safety', 'rust'), '?step=04-mcp-safety&lang=rust'); -assert.equal(lessonUrl('04-mcp-safety'), '?step=04-mcp-safety'); -assert.equal(firstLessonUrl('java'), 'workshop/step.html?step=00-preflight&lang=java'); -assert.equal(firstLessonUrl('python', 'museum'), 'workshop/step.html?step=museum-00-preflight&lang=python'); -assert.equal(homeUrl('python'), '../index.html?lang=python'); -assert.equal(homeUrl('python', 'museum'), '../index.html?lang=python&workshop=museum'); -assert.equal(homeUrl(), '../index.html'); -assert.equal( - siteRootUrl('https://expert-adventure-l67eo16.pages.github.io/workshop/step.html?step=00-preflight').href, - 'https://expert-adventure-l67eo16.pages.github.io/' -); -assert.equal( - siteRootUrl('https://github.github.io/copilot-sdk-workshop/workshop/step.html').href, - 'https://github.github.io/copilot-sdk-workshop/' -); -assert.equal(resolveLanguage('?lang=go', 'rust', getLanguage).id, 'go'); -assert.equal(resolveLanguage('', 'rust', getLanguage).id, 'rust'); -assert.equal(resolveLanguage('?lang=unknown', 'rust', getLanguage), null); - -console.log('Workshop language directive and navigation tests passed.'); diff --git a/docs/tests/navigation.test.js b/docs/tests/navigation.test.js new file mode 100644 index 0000000..8b271e1 --- /dev/null +++ b/docs/tests/navigation.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const { + firstLessonUrl, + homeUrl, + lessonUrl, + sdk, + siteRootUrl +} = require('../navigation.js'); +const { + format, + getLocale, + getMessages, + locales, + resolveLocale +} = require('../localization.js'); + +assert.equal(sdk.displayName, 'Node.js'); +assert.equal(lessonUrl('museum-04-approved-facts'), '?step=museum-04-approved-facts'); +assert.equal( + lessonUrl('museum-04-approved-facts', 'pt-BR'), + '?step=museum-04-approved-facts&locale=pt-BR' +); +assert.equal(firstLessonUrl(), 'workshop/step.html?step=museum-00-preflight'); +assert.equal(firstLessonUrl('pt-BR'), 'workshop/step.html?step=museum-00-preflight&locale=pt-BR'); +assert.equal(homeUrl(), '../index.html'); +assert.equal(homeUrl('pt-BR'), '../index.html?locale=pt-BR'); +assert.equal( + siteRootUrl('https://expert-adventure-l67eo16.pages.github.io/workshop/step.html?step=museum-00-preflight').href, + 'https://expert-adventure-l67eo16.pages.github.io/' +); +assert.equal( + siteRootUrl('https://github.github.io/copilot-sdk-workshop/workshop/step.html').href, + 'https://github.github.io/copilot-sdk-workshop/' +); + +assert.equal(resolveLocale('', null).id, 'en'); +assert.equal(resolveLocale('?locale=pt-BR', null).id, 'pt-BR'); +assert.equal(resolveLocale('?locale=PT-br', null).id, 'pt-BR'); +assert.equal(resolveLocale('', 'pt-BR').id, 'pt-BR'); +assert.equal(resolveLocale('?locale=unknown', 'pt-BR').id, 'pt-BR'); +assert.equal(getLocale('unknown'), null); +assert.equal(getMessages('unknown').pageTitle, getMessages('en').pageTitle); +assert.equal(format('Etapa {number} de {count}', { number: 2, count: 7 }), 'Etapa 2 de 7'); + +// Every locale must define the same message keys as English, so no UI string falls back silently. +const keysOf = value => Object.entries(value).flatMap(([key, nested]) => + nested && typeof nested === 'object' && key !== 'steps' + ? keysOf(nested).map(child => `${key}.${child}`) + : [key]); +const englishKeys = keysOf(getMessages('en')).sort(); +for (const locale of locales) { + assert.deepEqual(keysOf(getMessages(locale.id)).sort(), englishKeys, `${locale.id} message keys`); +} + +console.log('Workshop navigation and localization tests passed.'); diff --git a/docs/theme-toggle.js b/docs/theme-toggle.js index 7589c49..3e00853 100644 --- a/docs/theme-toggle.js +++ b/docs/theme-toggle.js @@ -14,8 +14,12 @@ function toggleTheme() { function updateToggleIcon() { const isLight = document.documentElement.dataset.theme === 'light'; document.querySelectorAll('.theme-toggle').forEach(button => { - button.textContent = isLight ? '🌙 Dark' : '☀️ Light'; - button.setAttribute('aria-label', `Switch to ${isLight ? 'dark' : 'light'} theme`); + const lightLabel = button.dataset.lightLabel ?? 'Light'; + const darkLabel = button.dataset.darkLabel ?? 'Dark'; + const switchToLight = button.dataset.switchToLight ?? 'Switch to light theme'; + const switchToDark = button.dataset.switchToDark ?? 'Switch to dark theme'; + button.textContent = isLight ? `🌙 ${darkLabel}` : `☀️ ${lightLabel}`; + button.setAttribute('aria-label', isLight ? switchToDark : switchToLight); }); } diff --git a/docs/workshop/step.html b/docs/workshop/step.html index 76d598f..dbc7e22 100644 --- a/docs/workshop/step.html +++ b/docs/workshop/step.html @@ -3,8 +3,8 @@ - - Copilot SDK Workshop + + Museum Exhibit Studio | Copilot SDK Workshop @@ -14,12 +14,11 @@ - - - + + - +
@@ -33,23 +32,18 @@ - Copilot SDK Workshop + Museum Exhibit Studio Loading lesson -
- - diff --git a/src/BlazorApp/Components/Layout/MainLayout.razor b/src/BlazorApp/Components/Layout/MainLayout.razor deleted file mode 100644 index d1b28e9..0000000 --- a/src/BlazorApp/Components/Layout/MainLayout.razor +++ /dev/null @@ -1,15 +0,0 @@ -@inherits LayoutComponentBase - -
- - -
- @Body -
- -
-

Built for the Copilot SDK Workshop

-
-
diff --git a/src/BlazorApp/Components/Pages/Home.razor b/src/BlazorApp/Components/Pages/Home.razor deleted file mode 100644 index 375f62f..0000000 --- a/src/BlazorApp/Components/Pages/Home.razor +++ /dev/null @@ -1,22 +0,0 @@ -@page "/" - -
-

Welcome to the Accessibility Report Target

-

This page intentionally contains accessibility issues for the workshop.

- - - -

Quick actions

-
- - -
- -

About this app

-

- The accessibility report generator uses the GitHub Copilot SDK and the Playwright MCP server - to analyze this page and produce a WCAG-aligned report. -

- - Learn more -
diff --git a/src/BlazorApp/Components/Routes.razor b/src/BlazorApp/Components/Routes.razor deleted file mode 100644 index f0c0911..0000000 --- a/src/BlazorApp/Components/Routes.razor +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
diff --git a/src/BlazorApp/Components/_Imports.razor b/src/BlazorApp/Components/_Imports.razor deleted file mode 100644 index f7e871b..0000000 --- a/src/BlazorApp/Components/_Imports.razor +++ /dev/null @@ -1,11 +0,0 @@ -@using System.Net.Http -@using System.Net.Http.Json -@using Microsoft.AspNetCore.Components -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.JSInterop -@using BlazorApp -@using BlazorApp.Components -@using BlazorApp.Components.Layout diff --git a/src/BlazorApp/Program.cs b/src/BlazorApp/Program.cs deleted file mode 100644 index f44960c..0000000 --- a/src/BlazorApp/Program.cs +++ /dev/null @@ -1,21 +0,0 @@ -using BlazorApp.Components; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddRazorComponents(); - -var app = builder.Build(); - -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); - app.UseHsts(); -} - -app.UseHttpsRedirection(); -app.UseStaticFiles(); -app.UseAntiforgery(); - -app.MapRazorComponents(); - -app.Run(); diff --git a/src/BlazorApp/wwwroot/app.css b/src/BlazorApp/wwwroot/app.css deleted file mode 100644 index 1014592..0000000 --- a/src/BlazorApp/wwwroot/app.css +++ /dev/null @@ -1,90 +0,0 @@ -html, body { - margin: 0; - padding: 0; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background-color: #f8f9fa; - color: #212529; -} - -.page { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.banner { - background-color: #343a40; - color: #fff; - padding: 1rem 2rem; -} - -.banner h2 { - margin: 0; - font-size: 1.25rem; -} - -.content { - flex: 1; - padding: 2rem; - max-width: 800px; - margin: 0 auto; -} - -.hero { - background: #fff; - border-radius: 8px; - padding: 2rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.hero h2 { - margin-top: 0; -} - -.muted { - color: #adb5bd; -} - -.hero img { - display: block; - margin: 1.5rem 0; - border-radius: 4px; -} - -form { - display: flex; - gap: 0.5rem; - margin: 1rem 0; -} - -input[type="text"] { - padding: 0.5rem; - border: 1px solid #ced4da; - border-radius: 4px; - flex: 1; -} - -button { - padding: 0.5rem 1rem; - background-color: #0d6efd; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -button:focus { - outline: none; -} - -.low-contrast-link { - color: #d0d0d0; - text-decoration: none; -} - -.footer { - background-color: #e9ecef; - padding: 1rem 2rem; - text-align: center; - color: #6c757d; -} diff --git a/start-accessibility/README.md b/start-accessibility/README.md deleted file mode 100644 index b26c57d..0000000 --- a/start-accessibility/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Workshop starters - -Choose the directory for the language selected on the workshop homepage, then work directly inside -it. There is no copy step. Change into that directory, open the same folder in your editor (`code .` -from inside it, or any other editor's open-folder command), and stay there for every command. -Starters are intentionally minimal scaffolds. The application-owned Web Content Accessibility -Guidelines (WCAG) catalog and scoped permission/snapshot-reader helpers may be present for later -lessons, but their executable entrypoints do not wire a Copilot client, session, streaming flow, -local tool, MCP server, or report until the corresponding step. - -| Language | Prerequisite | Change directory and verify | -|---|---|---| -| .NET | [.NET 10 SDK](https://learn.microsoft.com/dotnet/core/install/) | `cd start-accessibility/dotnet && dotnet build` | -| Node.js | [Node.js 22+](https://nodejs.org/) | `cd start-accessibility/nodejs && npm install && npm run build` | -| Python | [Python 3.11+](https://www.python.org/downloads/) | `cd start-accessibility/python && python -m pip install -r requirements.txt && python -m py_compile *.py` | -| Go | [Go 1.24+](https://go.dev/dl/) | `cd start-accessibility/go && go build -mod=readonly ./...` | -| Rust | [Rust 1.94+](https://rustup.rs/) | `cd start-accessibility/rust && cargo check --locked` | -| Java | [Java 17+](https://adoptium.net/) and [Maven](https://maven.apache.org/install.html) | `cd start-accessibility/java && mvn compile` | - -Because you edit these files in place, your work shows up in `git status`. That is expected. Run -`git checkout -- .` from the repository root to restore a clean starter. Go, Rust, and Java tracks -require the -[GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) on -`PATH` when you later run the application. SDK setup and API references are available in the -[official Copilot SDK repository](https://github.com/github/copilot-sdk) and -[cookbook](https://github.com/github/copilot-sdk/tree/main/cookbook). - -Stay in your starter directory for the whole workshop. Return to the interactive viewer from the -[workshop homepage](../README.md#start-the-workshop); do not open lesson Markdown directly. diff --git a/start-accessibility/dotnet/HelloCopilotSDK.csproj b/start-accessibility/dotnet/HelloCopilotSDK.csproj deleted file mode 100644 index e5c14ea..0000000 --- a/start-accessibility/dotnet/HelloCopilotSDK.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net10.0 - HelloCopilotSDK - enable - enable - - - - - - - \ No newline at end of file diff --git a/start-accessibility/dotnet/Helpers/AccessibilityRuleCatalog.cs b/start-accessibility/dotnet/Helpers/AccessibilityRuleCatalog.cs deleted file mode 100644 index b2ff284..0000000 --- a/start-accessibility/dotnet/Helpers/AccessibilityRuleCatalog.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace HelloCopilotSDK.Helpers; - -public static class AccessibilityRuleCatalog -{ - public static readonly AccessibilityRule[] Rules = - [ - new( - "1.1.1", - "Non-text Content", - "An informative image has no useful text alternative.", - "Add concise alt text that communicates the image's purpose. Use alt=\"\" only for decorative images.", - ["image", "alt text", "text alternative"]), - new( - "1.3.1", - "Info and Relationships", - "Page structure or relationships are only conveyed visually.", - "Use semantic landmarks and a logical heading hierarchy so structure is programmatically available.", - ["main landmark", "heading hierarchy", "page structure", "semantic"]), - new( - "1.4.3", - "Contrast (Minimum)", - "Text does not have enough contrast against its background.", - "Provide at least 4.5:1 contrast for normal text and 3:1 for large text.", - ["contrast", "low contrast", "color"]), - new( - "2.4.7", - "Focus Visible", - "Keyboard focus cannot be seen clearly.", - "Keep a visible, high-contrast focus indicator on every interactive element.", - ["focus", "keyboard", "outline"]), - new( - "3.3.2", - "Labels or Instructions", - "A form does not provide a persistent visible label or necessary instructions.", - "Provide visible labels and instructions that explain the expected input.", - ["visible label", "instructions", "required field", "input format"]), - new( - "4.1.2", - "Name, Role, Value", - "A form control has no programmatically determinable accessible name.", - "Associate a visible