This commit is contained in:
2026-04-19 15:23:12 -06:00
parent e4621ca65b
commit 883d43dca8
37 changed files with 619 additions and 12 deletions
+360
View File
@@ -0,0 +1,360 @@
---
order: 5
title: Lab 5 - API & Harnesses
description: Generate an Open WebUI API key, connect one of three coding harnesses, and build a small Zork-style game.
---
<!-- breakout-style: instruction-rails -->
<!-- step-style: underline -->
<!-- objective-style: divider -->
# Lab 5 - API & Harnesses
In this lab, we will:
- Generate a personal API key inside Open WebUI
- Install one of three coding harnesses
- Configure that harness to talk to Open WebUI as the backend
- Use the harness to build a small Zork-style game
<div class="lab-callout lab-callout--info">
<strong>Lab Flow Guide</strong><br />
This lab stays on a single high-level track, but Objectives 2 and 3 branch into three harness paths.<br />
Pick one harness, complete its branch, then rejoin the common Zork objective at the end.
</div>
To start this lab, one web service has been preconfigured:
- Open WebUI - http://<IP>:8080
## Objective 1 Execute: Generate an Open WebUI API Key
Before we install any harness, we need a key that lets the harness call the same model backend exposed through Open WebUI.
### Execute: Sign in to Open WebUI
1. Navigate to `http://<YOUR STUDENT IP>:8080`.
2. Sign in with the same account you used in Lab 4, or the credentials supplied by your instructor.
3. Confirm that you can reach the normal chat screen before continuing.
### Execute: Create a personal access token
According to the Open WebUI reference docs, API keys are created from **Settings -> Account** and authenticate with the same permissions as the user who created them.
1. Click your avatar in the lower-left corner.
<figure style="text-align: center;">
<a href="https://i.imgur.com/oZuZwWQ.png" target="_blank">
<img
src="https://i.imgur.com/oZuZwWQ.png"
style="width: 50%; display: block; margin-left: auto; margin-right: auto; border: 5px solid black;">
</a>
<figcaption style="margin-top: 8px; font-size: 1.1em;">
User Settings
</figcaption>
</figure>
<br>
2. Open **Settings**.
3. Open **Account**.
4. Locate the **API Keys** section.
<figure style="text-align: center;">
<a href="https://i.imgur.com/oDe6cpE.png" target="_blank">
<img
src="https://i.imgur.com/oDe6cpE.png"
style="width: 50%; display: block; margin-left: auto; margin-right: auto; border: 5px solid black;">
</a>
<figcaption style="margin-top: 8px; font-size: 1.1em;">
API Key
</figcaption>
</figure>
<br>
6. Copy the key immediately and store it somewhere safe for the duration of the lab.
<div class="lab-callout lab-callout--warning">
<strong>If you do not see API Keys:</strong> Open WebUI requires the feature to be enabled globally, and your user account needs permission to generate keys. Ask your instructor for help before continuing.
</div>
### Execute: Sanity-check the key from the terminal
Run a quick authenticated request against the Open WebUI model list endpoint. You should receive JSON back instead of an authentication error.
```bash
curl http://<YOUR STUDENT IP>:8080/api/models \
-H "Authorization: Bearer YOUR_OPENWEBUI_API_KEY"
```
If this request works, your harness will use the same key for later steps.
---
## Objective 2 Execute: Choose and Install a Harness
All three branches ultimately talk to the same Open WebUI backend. The difference is the user interface and configuration style for each harness.
<div class="lab-harness-chooser" role="group" aria-label="Harness installation paths">
<button type="button" class="lab-harness-card" data-harness-choice="opencode" aria-pressed="false">
<span class="lab-harness-card__tag">Path A</span>
<strong>OpenCode</strong>
<span>Terminal-first coding agent</span>
</button>
<button type="button" class="lab-harness-card" data-harness-choice="kilocode" aria-pressed="false">
<span class="lab-harness-card__tag">Path B</span>
<strong>Kilo Code VS Code</strong>
<span>Editor-driven coding assistant</span>
</button>
<button type="button" class="lab-harness-card" data-harness-choice="droid" aria-pressed="false">
<span class="lab-harness-card__tag">Path C</span>
<strong>Factory Droid</strong>
<span>Advanced CLI harness with powerful Spec Driven Development (Missions)</span>
</button>
</div>
<p>Select a path to reveal that harness's instructions throughout the rest of the lab. Select the same card again if you want to hide the harness-specific instructions and return to the shared overview.</p>
### Execute: Install the harness you want to use
<section class="lab-harness-branch" id="opencode-install" data-harness-branch="opencode">
<p class="lab-harness-branch__eyebrow">Path A</p>
<h3>Install OpenCode</h3>
<p>OpenCode is a terminal-native coding agent. Its official docs recommend either the install script or the npm package.</p>
<pre><code class="language-bash">curl -fsSL https://opencode.ai/install | bash
opencode --version</code></pre>
<p>If you prefer npm and already have Node.js installed:</p>
<pre><code class="language-bash">npm install -g opencode-ai
opencode --version</code></pre>
<p>Once installed, stay in the terminal. We will configure OpenCode in Objective 3.</p>
</section>
<section class="lab-harness-branch" id="kilocode-install" data-harness-branch="kilocode">
<p class="lab-harness-branch__eyebrow">Path B</p>
<h3>Install Kilo Code for VS Code</h3>
<p>Kilo Code is primarily used through the editor UI. For this Linux-first lab flow, use VS Code on the student workstation and install the extension from the marketplace.</p>
<ol>
<li>Open <strong>VS Code</strong>.</li>
<li>Open the <strong>Extensions</strong> view.</li>
<li>Search for <strong>Kilo Code</strong>.</li>
<li>Click <strong>Install</strong>.</li>
<li>Reload VS Code if prompted.</li>
<li>Open the project folder you want to work in before moving to Objective 3.</li>
</ol>
<div class="lab-callout lab-callout--info">
<strong>Tip:</strong> Kilo Code supports several providers and local-model options. In this lab, we will use its <strong>OpenAI Compatible</strong> provider flow so it can target Open WebUI.
</div>
</section>
<section class="lab-harness-branch" id="droid-install" data-harness-branch="droid">
<p class="lab-harness-branch__eyebrow">Path C</p>
<h3>Install Factory Droid</h3>
<p>Factory's Droid harness runs in the terminal and supports BYOK custom models through Factory configuration files.</p>
<pre><code class="language-bash">curl -fsSL https://app.factory.ai/cli | sh
droid --version</code></pre>
<p>If the shell needs to be reloaded after install, open a fresh terminal and rerun <code>droid --version</code>.</p>
</section>
---
## Objective 3 Execute: Configure Your Harness for Open WebUI
For all three harnesses, the common backend values are:
- `Base URL` - `http://<YOUR STUDENT IP>:8080/api`
- `API Key` - `YOUR_OPENWEBUI_API_KEY`
- `Model ID` - Any model ID returned by Open WebUI, such as `qwen3.5:4b`
The shared idea is simple: your harness sends requests to Open WebUI's authenticated API endpoints instead of directly to a cloud provider.
### Execute: Apply the configuration for your chosen harness
<section class="lab-harness-branch" id="opencode-config" data-harness-branch="opencode">
<p class="lab-harness-branch__eyebrow">Path A</p>
<h3>Configure OpenCode</h3>
<p>OpenCode supports OpenAI-compatible providers through its JSON config. Create either a project-local <code>opencode.json</code> file or a global config under <code>~/.config/opencode/opencode.json</code>.</p>
<p>It can also be easier to start opencode once, and exit with /exit. Use the following example to help structure your opencode.json file.
<pre><code class="language-json">{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openwebui": {
"name": "Open WebUI",
"options": {
"baseURL": "http://&lt;YOUR STUDENT IP&gt;:8080/api",
},
"models": {
"qwen3.5:4b": {
"name": "Qwen 3.5 4B"
}
}
}
},
"model": "openwebui/qwen3.5:4b"
}</code></pre>
<p>After saving the config, you can login with <code>opencode auth login: </code></p>
<figure style="text-align: center;">
<a href="https://i.imgur.com/wLPJOpz.png" target="_blank">
<img
src="https://i.imgur.com/wLPJOpz.png"
style="width: 50%; display: block; margin-left: auto; margin-right: auto; border: 5px solid black;">
</a>
<figcaption style="margin-top: 8px; font-size: 1.1em;">
opencode auth login
</figcaption>
</figure>
<p>After logging in, start OpenCode from your project directory:</p>
<pre><code class="language-bash">cd /path/to/your/project
opencode</code></pre>
</section>
<section class="lab-harness-branch" id="kilocode-config" data-harness-branch="kilocode">
<p class="lab-harness-branch__eyebrow">Path B</p>
<h3>Configure Kilo Code in VS Code</h3>
<p>Kilo Code's documented workflow is provider-driven through the extension settings UI. Use the following values when creating or editing your provider profile.</p>
<ul>
<li><code>API Provider</code> - <code>OpenAI Compatible</code></li>
<li><code>OpenAI Base URL</code> - <code>http://&lt;YOUR STUDENT IP&gt;:8080/api</code></li>
<li><code>API Key</code> - <code>YOUR_OPENWEBUI_API_KEY</code></li>
<li><code>Model ID</code> - <code>qwen3.5:4b</code> or another model exposed by Open WebUI</li>
<li><code>Approval Mode</code> - Leave the safer default enabled for your first run</li>
</ul>
<br>
<ol>
<li>Open the Kilo Code panel in VS Code.</li>
<li>Open its provider or API settings.</li>
<li>Select <strong>OpenAI Compatible</strong> as the provider.</li>
<li>Paste in the base URL and API key values above.</li>
<li>Pick a model ID that exists in Open WebUI.</li>
<li>Start a new task to verify Kilo Code can connect successfully.</li>
</ol>
<figure style="text-align: center;">
<a href="https://i.imgur.com/Q61IK03.png" target="_blank">
<img
src="https://i.imgur.com/Q61IK03.png"
style="width: 50%; display: block; margin-left: auto; margin-right: auto; border: 5px solid black;">
</a>
<figcaption style="margin-top: 8px; font-size: 1.1em;">
Kilo Code Settings
</figcaption>
</figure>
<br>
<figure style="text-align: center;">
<a href="https://i.imgur.com/vZV9qWW.png" target="_blank">
<img
src="https://i.imgur.com/vZV9qWW.png"
style="width: 50%; display: block; margin-left: auto; margin-right: auto; border: 5px solid black;">
</a>
<figcaption style="margin-top: 8px; font-size: 1.1em;">
Provider Settings
</figcaption>
</figure>
<br>
<div class="lab-callout lab-callout--info">
<strong>Tip:</strong> If model discovery fails, go back to your terminal and rerun the <code>curl /api/models</code> check from Objective 1. The harness and the curl command use the same authentication path.
</div>
</section>
<section class="lab-harness-branch" id="droid-config" data-harness-branch="droid">
<p class="lab-harness-branch__eyebrow">Path C</p>
<h3>Configure Factory Droid</h3>
<p>Factory's BYOK documentation supports custom model entries in <code>~/.factory/config.json</code>. Because Open WebUI exposes a chat-completions-compatible API, use the <code>generic-chat-completion-api</code> provider type.</p>
<pre><code class="language-json">{
"custom_models": [
{
"model_display_name": "Open WebUI - Qwen 3.5 4B",
"model": "qwen3.5:4b",
"base_url": "http://&lt;YOUR STUDENT IP&gt;:8080/api",
"api_key": "YOUR_OPENWEBUI_API_KEY",
"provider": "generic-chat-completion-api",
"max_tokens": 4096
}
]
}</code></pre>
<p>After saving the config:</p>
<ol>
<li>Launch <code>droid</code>.</li>
<li>Open the model selector with <code>/model</code>.</li>
<li>Choose your new custom Open WebUI model entry.</li>
<li>Start a new session in the target project directory.</li>
</ol>
</section>
---
## Objective 4 Execute: Build a Tiny Zork Clone
At this point, all three branches reconnect. The rest of the lab is the same no matter which harness you chose.
### Execute: Start your harness session
<div class="lab-harness-chooser" aria-label="Harness launch reminders">
<div class="lab-harness-card" data-harness-branch="opencode">
<span class="lab-harness-card__tag">OpenCode</span>
<strong>Terminal Session</strong>
<span>Run <code>opencode</code> inside the project directory.</span>
</div>
<div class="lab-harness-card" data-harness-branch="kilocode">
<span class="lab-harness-card__tag">Kilo Code</span>
<strong>VS Code Task</strong>
<span>Open the repo folder and start a new Kilo Code task from the side panel.</span>
</div>
<div class="lab-harness-card" data-harness-branch="droid">
<span class="lab-harness-card__tag">Factory Droid</span>
<strong>CLI Session</strong>
<span>Run <code>droid</code>, type "/mission", and ensure you've selected your custom model for each phase.</span>
</div>
</div>
### Execute: Give the harness a shared prompt
Use the following prompt as your starting task. Ensure you are in **Plan** mode (or a Droid Mission):
```text
You are helping me build a tiny terminal adventure game in Python.
Create a Zork-style prototype with:
- at least 5 connected rooms
- movement commands like north, south, east, and west
- a simple inventory system
- one collectible key
- one locked room or door
- a short win condition
Use clean, readable Python and keep everything runnable from the terminal.
After writing the code, explain how to launch the game and what commands the player can use.
```
### Explore: Execute the result
Once your harness produces the first version, keep pushing it with follow-up prompts:
1. Ask it to add a help command.
2. Ask it to improve room descriptions.
3. Ask it to prevent impossible movement.
4. Ask it to add one extra puzzle or hidden interaction.
Alternatively, reflect if you'd instead focused on using a Spec Driven development flow. How might the AI model perform more accurately as the requirements become more complicated?
### Checkpoint: What success can look like
Before finishing the lab, confirm that your game can:
1. Start from the terminal without errors.
2. Accept basic movement commands.
3. Let the player pick up at least one item.
4. Use that item to unlock progress.
5. Reach a clear win state.
## Conclusion
In this lab, we:
1. Generated an Open WebUI API key.
2. Installed a harness of our choice.
3. Connected that harness back to Open WebUI.
4. Used the harness to build a small but complete coding exercise.
You should now have a repeatable pattern for testing other harnesses against the same Open WebUI deployment. We've also shown how a full local stack can work, from model selection, inference, harness installation, to real coding work.
@@ -1,6 +1,6 @@
--- ---
order: 5 order: 6
title: Lab 5 - Embedding and Chunking title: Lab 6 - Embedding and Chunking
description: Explore chunking strategies and embeddings, then connect them to retrieval workflows. description: Explore chunking strategies and embeddings, then connect them to retrieval workflows.
--- ---
@@ -8,7 +8,7 @@ description: Explore chunking strategies and embeddings, then connect them to re
<!-- step-style: underline --> <!-- step-style: underline -->
<!-- objective-style: divider --> <!-- objective-style: divider -->
# Lab 5 - Embedding and Chunking # Lab 6 - Embedding and Chunking
In this lab, we will: In this lab, we will:

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Before

Width:  |  Height:  |  Size: 792 KiB

After

Width:  |  Height:  |  Size: 792 KiB

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 632 KiB

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,6 +1,6 @@
--- ---
order: 6 order: 7
title: Lab 6 - Dataset Generation and Fine Tuning title: Lab 7 - Dataset Generation and Fine Tuning
description: Review dataset options, generate examples with Kiln.ai, and fine-tune a model in Unsloth. description: Review dataset options, generate examples with Kiln.ai, and fine-tune a model in Unsloth.
--- ---
@@ -8,7 +8,7 @@ description: Review dataset options, generate examples with Kiln.ai, and fine-tu
<!-- step-style: underline --> <!-- step-style: underline -->
<!-- objective-style: divider --> <!-- objective-style: divider -->
# Lab 6 - Dataset Generation and Fine Tuning # Lab 7 - Dataset Generation and Fine Tuning
In this lab, we will: In this lab, we will:
@@ -1,6 +1,6 @@
--- ---
order: 7 order: 8
title: Lab 7 - Evaluation and Red Teaming title: Lab 8 - Evaluation and Red Teaming
description: Probe model defenses manually and with Promptfoo to evaluate security controls. description: Probe model defenses manually and with Promptfoo to evaluate security controls.
--- ---
@@ -8,7 +8,7 @@ description: Probe model defenses manually and with Promptfoo to evaluate securi
<!-- step-style: underline --> <!-- step-style: underline -->
<!-- objective-style: divider --> <!-- objective-style: divider -->
# Lab 7 - Evaluation and Red Teaming # Lab 8 - Evaluation and Red Teaming
In this lab, we will: In this lab, we will:
@@ -97,7 +97,7 @@ Promptfoo is available on our lab machine at http://<YOUR STUDENT IP>:15500. We
Promptfoo is designed to be approachable for both beginners and practitioners. Its wizard guides you through configuring the target, selecting datasets and mutation strategies, and tracking execution. Promptfoo is designed to be approachable for both beginners and practitioners. Its wizard guides you through configuring the target, selecting datasets and mutation strategies, and tracking execution.
<div class="lab-callout lab-callout--info"> <div class="lab-callout lab-callout--info">
<strong>Tip:</strong> Although the Promptfoo WebUI is convenient, it hides a critical configuration option for this lab inside the YAML file. Please use the provided configuration file: [lab-7-evaluation-and-red-teaming/promptfoo.yaml](content/labs/lab-7-evaluation-and-red-teaming/promptfoo.yaml). Upload it with the <strong>Load Config</strong> button in the lower-left corner, then proceed with the following screenshot steps. <strong>Tip:</strong> Although the Promptfoo WebUI is convenient, it hides a critical configuration option for this lab inside the YAML file. Please use the provided configuration file: [lab-8-evaluation-and-red-teaming/promptfoo.yaml](/labs/lab-8-evaluation-and-red-teaming/promptfoo.yaml). Upload it with the <strong>Load Config</strong> button in the lower-left corner, then proceed with the following screenshot steps.
</div> </div>
<figure style="text-align: center;"> <figure style="text-align: center;">
@@ -167,7 +167,7 @@ Promptfoo is highly flexible. Anything that involves mass evaluation of prompts
### Explore: Promptfoo evaluation workflow ### Explore: Promptfoo evaluation workflow
<div class="lab-callout lab-callout--info"> <div class="lab-callout lab-callout--info">
<strong>Tip:</strong> Please use the provided evaluation configuration file: [lab-7-evaluation-and-red-teaming/mmlu-promptfoo-config.yaml](content/labs/lab-7-evaluation-and-red-teaming/mmlu-promptfoo-config.yaml). Upload it with the <strong>Load Config</strong> button in the lower-left corner, then proceed with the following screenshot steps. <strong>Tip:</strong> Please use the provided evaluation configuration file: [lab-8-evaluation-and-red-teaming/mmlu-promptfoo-config.yaml](/labs/lab-8-evaluation-and-red-teaming/mmlu-promptfoo-config.yaml). Upload it with the <strong>Load Config</strong> button in the lower-left corner, then proceed with the following screenshot steps.
</div> </div>
<figure style="text-align: center;"> <figure style="text-align: center;">

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

+52 -1
View File
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { LabContent } from "~/components/labs/LabContent"; import { LabContent } from "~/components/labs/LabContent";
@@ -36,4 +36,55 @@ describe("LabContent", () => {
expect(screen.getByText("Tokenizer Playground")).toBeInTheDocument(); expect(screen.getByText("Tokenizer Playground")).toBeInTheDocument();
expect(screen.getByText("Visualize token confidence locally")).toBeInTheDocument(); expect(screen.getByText("Visualize token confidence locally")).toBeInTheDocument();
}); });
it("filters harness branches from a single Objective 2 selector", () => {
render(
<LabContent
className="lab-content"
html={[
'<div class="lab-harness-chooser">',
'<button type="button" class="lab-harness-card" data-harness-choice="opencode">OpenCode</button>',
'<button type="button" class="lab-harness-card" data-harness-choice="kilocode">Kilo Code</button>',
'<button type="button" class="lab-harness-card" data-harness-choice="droid">Factory Droid</button>',
"</div>",
'<section data-harness-branch="opencode"><h3>OpenCode Install</h3></section>',
'<section data-harness-branch="kilocode"><h3>Kilo Code Install</h3></section>',
'<section data-harness-branch="droid"><h3>Factory Droid Install</h3></section>',
'<section data-harness-branch="opencode"><h3>OpenCode Config</h3></section>',
'<section data-harness-branch="kilocode"><h3>Kilo Code Config</h3></section>',
].join("")}
/>,
);
const openCodeInstall = screen.getByText("OpenCode Install").closest("section");
const kiloCodeInstall = screen.getByText("Kilo Code Install").closest("section");
const droidInstall = screen.getByText("Factory Droid Install").closest("section");
const kiloCodeConfig = screen.getByText("Kilo Code Config").closest("section");
expect(openCodeInstall?.hidden).toBe(true);
expect(kiloCodeInstall?.hidden).toBe(true);
expect(droidInstall?.hidden).toBe(true);
expect(kiloCodeConfig?.hidden).toBe(true);
fireEvent.click(screen.getByRole("button", { name: "Kilo Code" }));
expect(screen.getByRole("button", { name: "Kilo Code" })).toHaveAttribute(
"aria-pressed",
"true",
);
expect(openCodeInstall?.hidden).toBe(true);
expect(kiloCodeInstall?.hidden).toBe(false);
expect(droidInstall?.hidden).toBe(true);
expect(kiloCodeConfig?.hidden).toBe(false);
fireEvent.click(screen.getByRole("button", { name: "Kilo Code" }));
expect(screen.getByRole("button", { name: "Kilo Code" })).toHaveAttribute(
"aria-pressed",
"false",
);
expect(openCodeInstall?.hidden).toBe(true);
expect(kiloCodeInstall?.hidden).toBe(true);
expect(droidInstall?.hidden).toBe(true);
});
}); });
+60
View File
@@ -198,6 +198,64 @@ async function copyTextToClipboard(text: string) {
} }
} }
function enhanceHarnessSelectors(root: HTMLElement) {
const harnessButtons = Array.from(
root.querySelectorAll<HTMLButtonElement>("button[data-harness-choice]"),
);
const harnessBranches = Array.from(
root.querySelectorAll<HTMLElement>("[data-harness-branch]"),
);
if (harnessButtons.length === 0 || harnessBranches.length === 0) {
return () => {};
}
const supportedHarnesses = new Set(
harnessButtons
.map((button) => button.dataset.harnessChoice?.trim())
.filter((value): value is string => Boolean(value)),
);
let selectedHarness: string | null = null;
const syncHarnessSelection = () => {
for (const button of harnessButtons) {
const harnessId = button.dataset.harnessChoice?.trim() ?? "";
const isSelected = selectedHarness !== null && harnessId === selectedHarness;
button.setAttribute("aria-pressed", isSelected ? "true" : "false");
button.dataset.selected = isSelected ? "true" : "false";
}
for (const branch of harnessBranches) {
const harnessId = branch.dataset.harnessBranch?.trim() ?? "";
const shouldHide =
selectedHarness === null || harnessId !== selectedHarness;
branch.hidden = shouldHide;
branch.setAttribute("aria-hidden", shouldHide ? "true" : "false");
}
};
syncHarnessSelection();
const handleHarnessClick = (event: Event) => {
const target = event.target as HTMLElement;
const button = target.closest<HTMLButtonElement>("button[data-harness-choice]");
if (!button || !root.contains(button)) return;
const harnessId = button.dataset.harnessChoice?.trim() ?? "";
if (!supportedHarnesses.has(harnessId)) return;
event.preventDefault();
selectedHarness = selectedHarness === harnessId ? null : harnessId;
syncHarnessSelection();
};
root.addEventListener("click", handleHarnessClick);
return () => {
root.removeEventListener("click", handleHarnessClick);
};
}
export function LabContent({ className, html }: LabContentProps) { export function LabContent({ className, html }: LabContentProps) {
const containerRef = useRef<HTMLElement>(null); const containerRef = useRef<HTMLElement>(null);
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null); const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
@@ -273,6 +331,7 @@ export function LabContent({ className, html }: LabContentProps) {
} }
enhanceSettingsLists(root); enhanceSettingsLists(root);
const cleanupHarnessSelectors = enhanceHarnessSelectors(root);
const handleRootClick = (event: Event) => { const handleRootClick = (event: Event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
@@ -320,6 +379,7 @@ export function LabContent({ className, html }: LabContentProps) {
root.addEventListener("click", handleRootClick); root.addEventListener("click", handleRootClick);
return () => { return () => {
cleanupHarnessSelectors();
root.removeEventListener("click", handleRootClick); root.removeEventListener("click", handleRootClick);
}; };
}, [html]); }, [html]);
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { getLabDocument, getLabSummaries } from "~/lib/labs";
describe("labs discovery", () => {
it("returns the renumbered labs in order including the new Lab 5", () => {
const labs = getLabSummaries();
expect(labs.map((lab) => lab.slug)).toEqual([
"lab-1-visualization-in-transformerlab",
"lab-2-quantization-tradeoffs",
"lab-3-llama-cpp-and-ollama",
"lab-4-oi-prompting",
"lab-5-api-and-harnesses",
"lab-6-embedding-and-chunking",
"lab-7-dataset-generation-and-fine-tuning",
"lab-8-evaluation-and-red-teaming",
]);
});
it("resolves the new Lab 5 document", () => {
const lab = getLabDocument("lab-5-api-and-harnesses");
expect(lab).not.toBeNull();
expect(lab?.title).toBe("Lab 5 - API & Harnesses");
expect(lab?.fileName).toBe("lab-5-api-and-harnesses.md");
});
});
+108
View File
@@ -417,6 +417,105 @@ ol {
white-space: nowrap; white-space: nowrap;
} }
.lab-content .lab-harness-chooser {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.9rem;
margin: 1rem 0 1.35rem;
}
.lab-content .lab-harness-card {
display: flex;
flex-direction: column;
gap: 0.38rem;
min-height: 100%;
padding: 0.95rem 1rem;
border: 1px solid #d9e4ed;
border-radius: 16px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 249, 253, 0.96)),
radial-gradient(circle at top right, rgba(248, 156, 39, 0.12), transparent 32%);
box-shadow: 0 12px 28px -28px rgba(15, 23, 42, 0.35);
color: #18384f;
text-decoration: none;
}
.lab-content button.lab-harness-card {
width: 100%;
cursor: pointer;
text-align: left;
font: inherit;
}
.lab-content a.lab-harness-card:hover,
.lab-content button.lab-harness-card:hover {
transform: translateY(-1px);
border-color: #f1bd70;
box-shadow: 0 20px 36px -30px rgba(15, 23, 42, 0.45);
}
.lab-content .lab-harness-card[aria-pressed="true"] {
border-color: #cf7a08;
background:
linear-gradient(180deg, rgba(255, 251, 243, 0.98), rgba(255, 244, 227, 0.96)),
radial-gradient(circle at top right, rgba(248, 156, 39, 0.18), transparent 32%);
box-shadow: 0 20px 36px -30px rgba(114, 63, 8, 0.5);
}
.lab-content .lab-harness-card strong {
color: #0f3d58;
font-size: 1rem;
line-height: 1.35;
}
.lab-content .lab-harness-card span {
color: #486073;
font-size: 0.92rem;
}
.lab-content .lab-harness-card__tag {
display: inline-flex;
align-items: center;
width: fit-content;
border-radius: 999px;
padding: 0.22rem 0.58rem;
background: #e7f2fb;
color: #0f5c8b;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.lab-content .lab-harness-branch {
margin: 1rem 0 1.4rem;
padding: 1rem 1.1rem 1.1rem;
border: 1px solid #dce7f0;
border-left: 6px solid #0f5c8b;
border-radius: 16px;
background:
linear-gradient(180deg, rgba(250, 252, 254, 0.98), rgba(244, 249, 252, 0.95)),
radial-gradient(circle at top right, rgba(248, 156, 39, 0.08), transparent 26%);
scroll-margin-top: 1.5rem;
}
.lab-content .lab-harness-branch__eyebrow {
margin: 0 0 0.4rem;
color: #9a5f00;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.lab-content .lab-harness-branch > h3 {
margin: 0 0 0.45rem;
}
.lab-content .lab-harness-branch > p:last-child {
margin-bottom: 0;
}
.lab-content hr { .lab-content hr {
margin: 2rem 0 1.4rem; margin: 2rem 0 1.4rem;
border-color: #d7dee6; border-color: #d7dee6;
@@ -1686,6 +1785,15 @@ ol {
padding: 0.52rem 0.75rem; padding: 0.52rem 0.75rem;
} }
.lab-content .lab-harness-chooser {
grid-template-columns: 1fr;
}
.lab-content .lab-harness-card,
.lab-content .lab-harness-branch {
padding: 0.85rem 0.9rem;
}
.lab-content ul.lab-settings-list .lab-setting-value { .lab-content ul.lab-settings-list .lab-setting-value {
justify-self: start; justify-self: start;
} }