Skip to content

Commit bf570bc

Browse files
ivorbCopilot
andcommitted
Add workshop, task explorer and JSON views of the consolidated labs
Three surfaces over the same content, each generated from the frontmatter of the pages under Instructions/Consolidated. Nothing is hard-coded and there is no generated file to keep in sync: add a task page and it appears in all three. - workshop.html - instructor-led core path, ordered by lab then task, with cumulative elapsed time, optional tasks listed per session, and gated tasks called out as demo-only with what they require. - explore.html - every task as a card, filterable by lab, section, level, duration and access. - labs.json - the same catalogue as a machine-readable endpoint, for anything that needs the data without scraping the pages. index.md is untouched: it still filters to /Instructions/Exercises, so the published lab list is unaffected. Verified by rendering all three against the real frontmatter. Totals match the values the labs previously stated by hand - A 35/145 min, B 35/110, C 30/120 - and labs.json parses as valid JSON. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dc4690cc-57d6-48a7-abc8-bc8570113ab1
1 parent aa53c76 commit bf570bc

3 files changed

Lines changed: 388 additions & 0 deletions

File tree

explore.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
title: Task explorer
3+
permalink: explore.html
4+
---
5+
6+
{%- comment -%}
7+
Task explorer. Renders every consolidated task from its own frontmatter, then
8+
filters client-side. The data attributes are generated, so a new task appears
9+
here automatically once its page exists - there is no list to maintain.
10+
{%- endcomment -%}
11+
12+
{%- assign consolidated = site.pages | where_exp: "p", "p.url contains '/Instructions/Consolidated/'" -%}
13+
{%- assign labs = consolidated | where_exp: "p", "p.lab.type == 'lab'" | sort: "lab.order" -%}
14+
{%- assign tasks = consolidated | where_exp: "p", "p.lab.type == 'task'" | sort: "lab.order" -%}
15+
16+
<style>
17+
.ex-filters { display: flex; flex-wrap: wrap; gap: 1.25rem; margin: 1rem 0 1.5rem;
18+
padding: .9rem 1rem; border: 1px solid #e4e4e7; border-radius: 8px; font-size: .9em; }
19+
.ex-filters label { display: block; font-size: .78em; text-transform: uppercase;
20+
letter-spacing: .06em; color: #6b7280; margin-bottom: .25rem; }
21+
.ex-filters select { padding: .3rem .5rem; border: 1px solid #d4d4d8; border-radius: 6px; }
22+
.ex-count { margin: 0 0 1rem; color: #6b7280; font-size: .9em; }
23+
24+
.ex-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr)); gap: 1rem; }
25+
.ex-card { border: 1px solid #e4e4e7; border-radius: 8px; padding: .9rem 1rem; }
26+
.ex-card h4 { margin: 0 0 .35rem; font-size: 1em; }
27+
.ex-card p { margin: .35rem 0 .6rem; font-size: .88em; color: #3f3f46; }
28+
.ex-tags { display: flex; flex-wrap: wrap; gap: .3rem; }
29+
.ex-tag { font-size: .72em; padding: .12em .5em; border-radius: 999px; background: #f1f1f3; color: #52525b; }
30+
.ex-tag.core { background: #e8f0fe; color: #1a45a5; }
31+
.ex-tag.gated { background: #fef3c7; color: #92400e; }
32+
.ex-bars { letter-spacing: .1em; }
33+
.ex-empty { padding: 2rem; text-align: center; color: #6b7280; }
34+
</style>
35+
36+
# Task explorer
37+
38+
Every task across the consolidated labs, filterable. Each card is generated from that task's
39+
own frontmatter, so this list stays in step with the labs automatically.
40+
41+
Looking for a guided route instead? See the [workshop agenda]({{ '/workshop.html' | relative_url }}).
42+
43+
<div class="ex-filters" markdown="0">
44+
<div>
45+
<label for="f-lab">Lab</label>
46+
<select id="f-lab">
47+
<option value="">All labs</option>
48+
{%- for lab in labs %}
49+
<option value="{{ lab.lab.id }}">{{ lab.lab.title }}</option>
50+
{%- endfor %}
51+
</select>
52+
</div>
53+
<div>
54+
<label for="f-section">Section</label>
55+
<select id="f-section">
56+
<option value="">Core and optional</option>
57+
<option value="core">Core only</option>
58+
<option value="optional">Optional only</option>
59+
<option value="setup">Setup</option>
60+
</select>
61+
</div>
62+
<div>
63+
<label for="f-level">Level</label>
64+
<select id="f-level">
65+
<option value="">Any level</option>
66+
<option value="200">L200</option>
67+
<option value="300">L300</option>
68+
<option value="400">L400</option>
69+
</select>
70+
</div>
71+
<div>
72+
<label for="f-time">Time</label>
73+
<select id="f-time">
74+
<option value="">Any length</option>
75+
<option value="20">20 min or less</option>
76+
<option value="30">30 min or less</option>
77+
</select>
78+
</div>
79+
<div>
80+
<label for="f-access">Access</label>
81+
<select id="f-access">
82+
<option value="">All tasks</option>
83+
<option value="open">No extra access needed</option>
84+
<option value="gated">Needs extra access</option>
85+
</select>
86+
</div>
87+
</div>
88+
89+
<p class="ex-count" id="ex-count"></p>
90+
91+
<div class="ex-grid" id="ex-grid" markdown="0">
92+
{%- for t in tasks -%}
93+
{%- assign parent = labs | where_exp: "l", "l.lab.id == t.lab.parent" | first -%}
94+
<article class="ex-card"
95+
data-lab="{{ t.lab.parent }}"
96+
data-section="{{ t.lab.section }}"
97+
data-level="{{ t.lab.level }}"
98+
data-duration="{{ t.lab.duration | default: 0 }}"
99+
data-access="{{ t.lab.access }}">
100+
<h4><a href="{{ t.url | relative_url }}">{{ t.lab.title }}</a></h4>
101+
<p>{{ t.lab.description }}</p>
102+
<div class="ex-tags">
103+
<span class="ex-tag">{{ parent.lab.title }}</span>
104+
<span class="ex-tag {% if t.lab.section == 'core' %}core{% endif %}">{{ t.lab.section }}</span>
105+
{%- if t.lab.difficulty %}
106+
<span class="ex-tag ex-bars">{% for i in (1..5) %}{% if i <= t.lab.difficulty %}&#9648;{% else %}&#9649;{% endif %}{% endfor %} L{{ t.lab.level }}</span>
107+
{%- endif %}
108+
{%- if t.lab.duration %}<span class="ex-tag">{{ t.lab.duration }} min</span>{% endif %}
109+
{%- if t.lab.access == 'gated' %}<span class="ex-tag gated">needs access</span>{% endif %}
110+
</div>
111+
</article>
112+
{%- endfor %}
113+
</div>
114+
115+
<p class="ex-empty" id="ex-empty" style="display:none">No tasks match those filters.</p>
116+
117+
<script>
118+
(function () {
119+
var grid = document.getElementById('ex-grid');
120+
var cards = Array.prototype.slice.call(grid.querySelectorAll('.ex-card'));
121+
var count = document.getElementById('ex-count');
122+
var empty = document.getElementById('ex-empty');
123+
var filters = ['lab', 'section', 'level', 'time', 'access'].map(function (id) {
124+
return document.getElementById('f-' + id);
125+
});
126+
127+
function apply() {
128+
var lab = filters[0].value, section = filters[1].value, level = filters[2].value;
129+
var time = filters[3].value, access = filters[4].value;
130+
var shown = 0;
131+
132+
cards.forEach(function (card) {
133+
var d = card.dataset;
134+
var ok = (!lab || d.lab === lab)
135+
&& (!section || d.section === section)
136+
&& (!level || d.level === level)
137+
&& (!time || (parseInt(d.duration, 10) > 0 && parseInt(d.duration, 10) <= parseInt(time, 10)))
138+
&& (!access || d.access === access);
139+
card.style.display = ok ? '' : 'none';
140+
if (ok) { shown++; }
141+
});
142+
143+
count.textContent = shown + ' of ' + cards.length + ' tasks';
144+
empty.style.display = shown === 0 ? '' : 'none';
145+
}
146+
147+
filters.forEach(function (f) { f.addEventListener('change', apply); });
148+
apply();
149+
})();
150+
</script>

labs.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
permalink: labs.json
3+
layout: null
4+
---
5+
{%- comment -%}
6+
Machine-readable index of the consolidated labs, generated from the same page
7+
frontmatter that drives the site. Any consumer that needs the catalogue - a
8+
workshop scheduler, an LMS import, another site - can read this instead of
9+
scraping the pages or keeping its own copy.
10+
{%- endcomment -%}
11+
{%- assign consolidated = site.pages | where_exp: "p", "p.url contains '/Instructions/Consolidated/'" -%}
12+
{%- assign labs = consolidated | where_exp: "p", "p.lab.type == 'lab'" | sort: "lab.order" -%}
13+
{%- assign all_tasks = consolidated | where_exp: "p", "p.lab.type == 'task'" -%}
14+
{
15+
"generated_by": "labs.json template, from page frontmatter",
16+
"labs": [
17+
{%- for lab in labs -%}
18+
{%- assign lab_id = lab.lab.id -%}
19+
{%- assign tasks = all_tasks | where_exp: "p", "p.lab.parent == lab_id" | sort: "lab.order" -%}
20+
{%- assign core_total = 0 -%}
21+
{%- assign full_total = 0 -%}
22+
{%- for t in tasks -%}
23+
{%- if t.lab.duration -%}
24+
{%- assign full_total = full_total | plus: t.lab.duration -%}
25+
{%- if t.lab.section == 'core' %}{% assign core_total = core_total | plus: t.lab.duration %}{% endif -%}
26+
{%- endif -%}
27+
{%- endfor %}
28+
{
29+
"id": {{ lab.lab.id | jsonify }},
30+
"order": {{ lab.lab.order }},
31+
"title": {{ lab.lab.title | jsonify }},
32+
"description": {{ lab.lab.description | jsonify }},
33+
"level": {{ lab.lab.level }},
34+
"difficulty": {{ lab.lab.difficulty }},
35+
"status": {{ lab.lab.status | jsonify }},
36+
"concepts": {{ lab.lab.concepts | split: ", " | jsonify }},
37+
"url": {{ lab.url | relative_url | jsonify }},
38+
"duration_core_minutes": {{ core_total }},
39+
"duration_total_minutes": {{ full_total }},
40+
"tasks": [
41+
{%- for t in tasks %}
42+
{
43+
"order": {{ t.lab.order }},
44+
"title": {{ t.lab.title | jsonify }},
45+
"description": {{ t.lab.description | jsonify }},
46+
"section": {{ t.lab.section | jsonify }},
47+
"level": {{ t.lab.level }},
48+
{% if t.lab.difficulty %}"difficulty": {{ t.lab.difficulty }},{% else %}"difficulty": null,{% endif %}
49+
{% if t.lab.duration %}"duration_minutes": {{ t.lab.duration }},{% else %}"duration_minutes": null,{% endif %}
50+
"access": {{ t.lab.access | jsonify }},
51+
{% if t.lab.requires %}"requires": {{ t.lab.requires | jsonify }},{% else %}"requires": null,{% endif %}
52+
{% if t.lab.verify %}"verify": {{ t.lab.verify | jsonify }},{% else %}"verify": null,{% endif %}
53+
"concepts": {{ t.lab.concepts | split: ", " | jsonify }},
54+
"url": {{ t.url | relative_url | jsonify }}
55+
}{% unless forloop.last %},{% endunless %}
56+
{%- endfor %}
57+
]
58+
}{% unless forloop.last %},{% endunless %}
59+
{%- endfor %}
60+
]
61+
}

workshop.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: Workshop agenda
3+
permalink: workshop.html
4+
---
5+
6+
{%- comment -%}
7+
Workshop view. Every lab, task, timing and level on this page is read from the
8+
frontmatter of the pages under /Instructions/Consolidated/, so it cannot drift
9+
from the labs themselves. Nothing here is hard-coded.
10+
11+
Layout deliberately echoes a docs-style workshop site: a nav rail on the left
12+
and the agenda on the right.
13+
{%- endcomment -%}
14+
15+
{%- assign consolidated = site.pages | where_exp: "p", "p.url contains '/Instructions/Consolidated/'" -%}
16+
{%- assign labs = consolidated | where_exp: "p", "p.lab.type == 'lab'" | sort: "lab.order" -%}
17+
{%- assign all_tasks = consolidated | where_exp: "p", "p.lab.type == 'task'" -%}
18+
19+
<style>
20+
.wk-wrap { display: grid; grid-template-columns: 15rem 1fr; gap: 2.5rem; align-items: start; }
21+
@media (max-width: 900px) { .wk-wrap { grid-template-columns: 1fr; } }
22+
23+
.wk-nav { position: sticky; top: 1rem; font-size: .9em; border-right: 1px solid #e4e4e7; padding-right: 1rem; }
24+
.wk-nav h4 { margin: 0 0 .5rem; font-size: .75em; letter-spacing: .08em; text-transform: uppercase; color: #6b7280; }
25+
.wk-nav ol { list-style: none; margin: 0 0 1.25rem; padding: 0; }
26+
.wk-nav li { margin: 0 0 .35rem; }
27+
.wk-nav a { text-decoration: none; }
28+
.wk-nav a:hover { text-decoration: underline; }
29+
.wk-nav .wk-time { color: #6b7280; font-size: .85em; }
30+
31+
.wk-lab { border: 1px solid #e4e4e7; border-radius: 8px; padding: 1rem 1.25rem; margin: 0 0 1.25rem; }
32+
.wk-lab h3 { margin-top: 0; }
33+
.wk-meta { font-size: .85em; color: #6b7280; margin: -.4rem 0 .9rem; }
34+
.wk-step { display: grid; grid-template-columns: 1fr auto auto; gap: .75rem;
35+
padding: .45rem 0; border-top: 1px solid #f1f1f3; align-items: baseline; }
36+
.wk-step:first-of-type { border-top: none; }
37+
.wk-badge { font-size: .72em; padding: .12em .5em; border-radius: 999px; white-space: nowrap; }
38+
.wk-core { background: #e8f0fe; color: #1a45a5; }
39+
.wk-setup { background: #f1f1f3; color: #52525b; }
40+
.wk-elapsed { color: #6b7280; font-size: .85em; white-space: nowrap; }
41+
.wk-locked { color: #92400e; background: #fef3c7; }
42+
.wk-optional-list { font-size: .92em; color: #3f3f46; margin-top: .85rem; }
43+
</style>
44+
45+
<div class="wk-wrap" markdown="0">
46+
<nav class="wk-nav">
47+
<h4>Workshop</h4>
48+
<ol>
49+
<li><a href="#agenda">Agenda</a></li>
50+
<li><a href="#before-you-start">Before you start</a></li>
51+
</ol>
52+
<h4>Sessions</h4>
53+
<ol>
54+
{%- for lab in labs -%}
55+
{%- assign lab_id = lab.lab.id -%}
56+
{%- assign core = all_tasks | where_exp: "p", "p.lab.parent == lab_id" | where_exp: "p", "p.lab.section == 'core'" -%}
57+
{%- assign mins = 0 -%}
58+
{%- for t in core %}{% assign mins = mins | plus: t.lab.duration %}{% endfor -%}
59+
<li><a href="#lab-{{ lab_id | downcase }}">{{ lab.lab.title }}</a><br><span class="wk-time">{{ mins }} min core</span></li>
60+
{%- endfor -%}
61+
</ol>
62+
<h4>Other views</h4>
63+
<ol>
64+
<li><a href="{{ '/explore.html' | relative_url }}">Task explorer</a></li>
65+
<li><a href="{{ '/index.html' | relative_url }}">Full lab catalogue</a></li>
66+
</ol>
67+
</nav>
68+
69+
<div class="wk-main" markdown="1">
70+
71+
# AI agents workshop
72+
73+
A guided, instructor-led path through the Tailwind Traders labs. Each session builds a
74+
working agent, then hands off to the next.
75+
76+
This page is the **core path**: the shortest route through every lab that still ends with
77+
something running. Optional tasks are listed under each session for anyone who finishes early
78+
or wants to go further afterwards.
79+
80+
{%- assign total_core = 0 -%}
81+
{%- assign total_all = 0 -%}
82+
{%- for t in all_tasks -%}
83+
{%- if t.lab.duration -%}
84+
{%- assign total_all = total_all | plus: t.lab.duration -%}
85+
{%- if t.lab.section == 'core' %}{% assign total_core = total_core | plus: t.lab.duration %}{% endif -%}
86+
{%- endif -%}
87+
{%- endfor -%}
88+
{%- assign core_h = total_core | divided_by: 60 -%}
89+
{%- assign core_m = total_core | modulo: 60 -%}
90+
{%- assign all_h = total_all | divided_by: 60 -%}
91+
{%- assign all_m = total_all | modulo: 60 -%}
92+
93+
**Core path:** {{ labs.size }} sessions, about **{% if core_h > 0 %}{{ core_h }}h {% endif %}{{ core_m }}m** of
94+
hands-on time. **Everything**, including all optional tasks: about **{{ all_h }}h {% if all_m > 0 %}{{ all_m }}m{% endif %}**.
95+
96+
## Agenda
97+
{: #agenda }
98+
99+
{% assign elapsed = 0 %}
100+
{%- for lab in labs -%}
101+
{%- assign lab_id = lab.lab.id -%}
102+
{%- assign tasks = all_tasks | where_exp: "p", "p.lab.parent == lab_id" | sort: "lab.order" -%}
103+
{%- assign core = tasks | where_exp: "p", "p.lab.section == 'core'" -%}
104+
{%- assign optional = tasks | where_exp: "p", "p.lab.section == 'optional'" -%}
105+
{%- assign setup = tasks | where_exp: "p", "p.lab.section == 'setup'" -%}
106+
{%- assign lab_core = 0 -%}
107+
{%- for t in core %}{% assign lab_core = lab_core | plus: t.lab.duration %}{% endfor -%}
108+
109+
<div class="wk-lab" markdown="0" id="lab-{{ lab_id | downcase }}">
110+
<h3><a href="{{ lab.url | relative_url }}">Session {{ lab.lab.order }} &mdash; {{ lab.lab.title }}</a></h3>
111+
<p class="wk-meta">{{ lab_core }} min core &middot; L{{ lab.lab.level }} &middot; {{ lab.lab.concepts }}</p>
112+
113+
{%- for t in setup %}
114+
<div class="wk-step">
115+
<span><a href="{{ t.url | relative_url }}">{{ t.lab.title }}</a></span>
116+
<span class="wk-badge wk-setup">setup</span>
117+
<span class="wk-elapsed">before you start</span>
118+
</div>
119+
{%- endfor -%}
120+
121+
{%- for t in core %}
122+
{%- assign elapsed = elapsed | plus: t.lab.duration -%}
123+
{%- assign e_h = elapsed | divided_by: 60 -%}
124+
{%- assign e_m = elapsed | modulo: 60 -%}
125+
<div class="wk-step">
126+
<span><a href="{{ t.url | relative_url }}">{{ t.lab.title }}</a></span>
127+
<span class="wk-badge wk-core">{{ t.lab.duration }} min</span>
128+
<span class="wk-elapsed">{% if e_h > 0 %}{{ e_h }}h {% endif %}{{ e_m }}m elapsed</span>
129+
</div>
130+
{%- endfor -%}
131+
132+
{%- if optional.size > 0 %}
133+
<div class="wk-optional-list" markdown="0">
134+
<strong>Optional, if there's time:</strong>
135+
<ul>
136+
{%- for t in optional %}
137+
<li><a href="{{ t.url | relative_url }}">{{ t.lab.title }}</a> &middot; {{ t.lab.duration }} min &middot; L{{ t.lab.level }}
138+
{%- if t.lab.access == 'gated' %} <span class="wk-badge wk-locked">demo only &mdash; {{ t.lab.requires }}</span>{% endif -%}
139+
</li>
140+
{%- endfor %}
141+
</ul>
142+
</div>
143+
{%- endif %}
144+
</div>
145+
{%- endfor %}
146+
147+
## Before you start
148+
{: #before-you-start }
149+
150+
Each session has its own **Getting started** page covering the Microsoft Foundry project,
151+
starter code and environment for that lab. Attendees should complete the one for session 1
152+
before the workshop begins:
153+
154+
{% for lab in labs -%}
155+
{%- assign lab_id = lab.lab.id -%}
156+
{%- assign setup = all_tasks | where_exp: "p", "p.lab.parent == lab_id" | where_exp: "p", "p.lab.section == 'setup'" -%}
157+
{%- for s in setup %}
158+
- **{{ lab.lab.title }}** &mdash; [{{ s.lab.title }}]({{ s.url | relative_url }})
159+
{%- endfor -%}
160+
{%- endfor %}
161+
162+
{% assign gated = all_tasks | where_exp: "p", "p.lab.access == 'gated'" | sort: "lab.parent" %}
163+
{%- if gated.size > 0 %}
164+
### Tasks that need extra access
165+
166+
{{ gated.size }} optional tasks need permissions or licensing many attendees won't have, so
167+
they're **demo only** in a workshop setting. Nothing else depends on them.
168+
169+
| Task | Needs |
170+
| --- | --- |
171+
{% for t in gated -%}
172+
| [{{ t.lab.title }}]({{ t.url | relative_url }}) | {{ t.lab.requires }} |
173+
{% endfor %}
174+
{%- endif %}
175+
176+
</div>
177+
</div>

0 commit comments

Comments
 (0)