Add
@@ -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://<YOUR STUDENT IP>: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://<YOUR STUDENT IP>: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://<YOUR STUDENT IP>: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
|
||||
title: Lab 5 - Embedding and Chunking
|
||||
order: 6
|
||||
title: Lab 6 - Embedding and Chunking
|
||||
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 -->
|
||||
<!-- objective-style: divider -->
|
||||
|
||||
# Lab 5 - Embedding and Chunking
|
||||
# Lab 6 - Embedding and Chunking
|
||||
|
||||
In this lab, we will:
|
||||
|
||||
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 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: 294 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
order: 6
|
||||
title: Lab 6 - Dataset Generation and Fine Tuning
|
||||
order: 7
|
||||
title: Lab 7 - Dataset Generation and Fine Tuning
|
||||
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 -->
|
||||
<!-- objective-style: divider -->
|
||||
|
||||
# Lab 6 - Dataset Generation and Fine Tuning
|
||||
# Lab 7 - Dataset Generation and Fine Tuning
|
||||
|
||||
In this lab, we will:
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
order: 7
|
||||
title: Lab 7 - Evaluation and Red Teaming
|
||||
order: 8
|
||||
title: Lab 8 - Evaluation and Red Teaming
|
||||
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 -->
|
||||
<!-- objective-style: divider -->
|
||||
|
||||
# Lab 7 - Evaluation and Red Teaming
|
||||
# Lab 8 - Evaluation and Red Teaming
|
||||
|
||||
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.
|
||||
|
||||
<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>
|
||||
|
||||
<figure style="text-align: center;">
|
||||
@@ -167,7 +167,7 @@ Promptfoo is highly flexible. Anything that involves mass evaluation of prompts
|
||||
### Explore: Promptfoo evaluation workflow
|
||||
|
||||
<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>
|
||||
|
||||
<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 |
@@ -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 { LabContent } from "~/components/labs/LabContent";
|
||||
@@ -36,4 +36,55 @@ describe("LabContent", () => {
|
||||
expect(screen.getByText("Tokenizer Playground")).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
|
||||
@@ -273,6 +331,7 @@ export function LabContent({ className, html }: LabContentProps) {
|
||||
}
|
||||
|
||||
enhanceSettingsLists(root);
|
||||
const cleanupHarnessSelectors = enhanceHarnessSelectors(root);
|
||||
|
||||
const handleRootClick = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
@@ -320,6 +379,7 @@ export function LabContent({ className, html }: LabContentProps) {
|
||||
|
||||
root.addEventListener("click", handleRootClick);
|
||||
return () => {
|
||||
cleanupHarnessSelectors();
|
||||
root.removeEventListener("click", handleRootClick);
|
||||
};
|
||||
}, [html]);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -417,6 +417,105 @@ ol {
|
||||
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 {
|
||||
margin: 2rem 0 1.4rem;
|
||||
border-color: #d7dee6;
|
||||
@@ -1686,6 +1785,15 @@ ol {
|
||||
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 {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||