Compare commits

...
4 changed files with 537 additions and 1 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ tutorial(
if (await install.isEnabled().catch(() => false)) {
// Install sits low on the page: bring it up out of the caption's band first.
await t.scrollTo(install, { at: 0.45 });
// One literal per line: voice/speak.py only voices t.say("…") written out, never a ternary.
// One written-out line per branch: voice/speak.py never voices a ternary inside say().
if (clean) await t.say("Install puts it in your library as one bundle, ready for a Context.");
else await t.say("A warning or two can stay: Install puts it in your library as one bundle, and you fix the rest in its editors.");
await t.click(install);
+160
View File
@@ -0,0 +1,160 @@
// Nodes 1: WP Context in depth. Rows run top to bottom, so a combine above what it reads comes out
// empty (Move to bottom fixes it; a row's handle drags it, onto another Context too). The tick box
// switches a row off without removing it. The three row buttons. A second Context downstream starts
// with everything upstream; its own Role replaces the first one's (the blue info dot), and what was
// built upstream from the old value stays built. The right-click menu: Hold across run, Push vs
// Save to library, Wrap into bundle, Move. Prompt-only: no model. T09 already shows the seed modes,
// a lock, Edit for one node and the mod badge; 08 shows bundles.
import { StepError } from "watchwright";
import { tutorial } from "../tutorial.js";
tutorial(
"N01",
{
title: "WP Context in depth",
description: "How a Context runs its rows: order, switching a row off, the row buttons and markers, what comes from upstream, and the right-click menu.",
learn: "Run order, the tick box, lock / globe / bin, info and warning dots, upstream Contexts, Hold across run, Push vs Save to library, Wrap into bundle, Move.",
},
async (t) => {
const page = t.page;
const preview = (asm: string) => t.node(asm).locator('[data-test="asm-preview"]');
const menuOn = async (ctx: string, name: string) => {
await t.click(t.moduleRow(ctx, name).locator(".wp-module-name"), { right: true });
await t.beat(600);
return page.getByRole("menu").last();
};
const previewHas = async (asm: string, re: RegExp, what: string) => {
for (let i = 0; i < 20; i++) {
if (re.test(await preview(asm).innerText().catch(() => ""))) return;
await t.beat(300);
}
throw new StepError(`The Assembler preview never showed ${what}.`);
};
const n = await t.h.step("Rows run top to bottom", async () => {
await t.go("/");
await t.speedUp();
const ctx = await t.addNode("WP Context", { x: 480, y: 170 });
// Subject first, on purpose: it reads $role, $hair and $eyes, which come after it.
await t.pickModules(ctx, ["Subject", "Role", "Hair", "Eyes"]);
const asm = await t.addNode("WP Prompt Assembler", { x: 1180, y: 170 });
await t.wire(ctx, "context", asm, "context");
await t.click(t.node(asm).locator("textarea, [contenteditable='true']").first());
await t.type("A painting of $subject");
await t.press("Escape");
await t.beat(800);
await t.normalSpeed();
await t.say("A Context runs its rows from top to bottom, like a recipe. Here Subject sits first, and it joins the role, the hair and the eyes.");
await t.waitVoice();
await t.callout("prev", preview(asm), "Subject ran too early", 6);
await t.say("But when Subject ran, nothing below it had rolled yet, so it came out with empty gaps.");
await t.waitVoice();
await t.uncallout("prev");
await t.say("Right-click Subject, and Move to bottom.");
const menu = await menuOn(ctx, "Subject");
await t.click(menu.getByRole("menuitem", { name: "Move to bottom" }));
await t.beat(900);
await previewHas(asm, /with .+ and .+/, "a full subject");
await t.callout("prev", preview(asm), "Now it has everything", 6);
await t.say("Now it runs last and reads all three. A rule, a combine or a constraint goes below the lists it reads.");
await t.waitVoice();
await t.uncallout("prev");
const handle = t.moduleRow(ctx, "Role").locator(".wp-drag-handle");
await t.callout("handle", handle, "Drag to reorder", 4);
await t.say("You can also drag a row by its handle. Drop it on another Context to move it there.");
await t.waitVoice();
await t.uncallout("handle");
return { ctx, asm };
});
await t.h.step("Switch a row off", async () => {
const box = t.moduleRow(n.ctx, "Eyes").locator('[data-test="module-enabled"]');
await t.callout("tick", box, "Off, but kept", 4);
await t.say("The tick box switches a row off without removing it. Untick Eyes.");
await t.click(box);
await t.beat(800);
await t.waitVoice();
await t.uncallout("tick");
await t.callout("prev", preview(n.asm), "No eyes this time", 6);
await t.say("Eyes doesn't run, so Subject has nothing for them. WP Debug lists a switched-off row as skipped, and its settings stay for when you tick it again.");
await t.waitVoice();
await t.uncallout("prev");
await t.click(box);
await t.beat(600);
});
await t.h.step("The row buttons", async () => {
const row = t.moduleRow(n.ctx, "Role");
const buttons = [row.locator('[data-test="row-action-lock"]'), row.locator('[data-test="row-action-internal"]'), row.locator('[data-test="row-action-remove"]')];
await t.callout("btns", buttons, "Lock, globe, bin", 4);
await t.say("Every row has three buttons. The lock keeps the pick it rolled. The globe keeps its variable out of the prompt, for rules only. The bin removes the row.");
await t.waitVoice();
await t.uncallout("btns");
await t.say("If they crowd the node, the Display playground in Settings can show them only on hover.");
await t.waitVoice();
});
const ctx2 = await t.h.step("What comes from upstream", async () => {
await t.say("Contexts chain. Add a second one and wire the first into its upstream.");
const c2 = await t.addNode("WP Context", { x: 820, y: 640 });
await t.wire(n.ctx, "context", c2, "upstream");
await t.wire(c2, "context", n.asm, "context");
await t.beat(800);
await t.waitVoice();
await t.callout("prev", preview(n.asm), "Everything from upstream", 6);
await t.say("It starts with everything the first one made, so the Assembler, now after it, still prints the subject.");
await t.waitVoice();
await t.uncallout("prev");
await t.say("Now give the second Context its own Role.");
await t.pickModules(c2, ["Role"]);
await t.beat(900);
await t.waitVoice();
const dot = t.moduleRow(c2, "Role").locator(".wp-conflict-dot, .wp-conflict-badge").first();
await t.callout("dot", dot, "Replaces the one upstream", 4);
await t.say("The blue dot is information: this Role writes over the one from upstream. Amber means have a look. Neither ever stops a run. Hover a dot to read it.");
await t.waitVoice();
await t.uncallout("dot");
await t.click(t.node(n.asm).locator("textarea, [contenteditable='true']").first());
await t.press("ControlOrMeta+End");
await t.type(", $role");
await t.press("Escape");
await t.beat(900);
await t.callout("prev", preview(n.asm), "Two different roles", 6);
await t.say("Subject was built upstream, with the first role. A later write changes $role, but it doesn't rebuild what was already made from it.");
await t.waitVoice();
await t.uncallout("prev");
return c2;
});
await t.h.step("The right-click menu", async () => {
await t.say("Right-click a row for the rest.");
const menu = await menuOn(n.ctx, "Hair");
await t.callout("menu", menu, "Everything else for this row", 8);
await t.say("Edit opens it here. Hold across run keeps one pick for a whole batch or loop.");
await t.waitVoice();
await t.uncallout("menu");
await t.click(menu.getByRole("menuitem", { name: "Hold across run" }));
await t.beat(700);
await t.callout("held", t.moduleRow(n.ctx, "Hair").locator('[data-test="mod-held"]'), "Held", 4);
await t.say("The row says held. Every image of the next batch gets the same hair.");
await t.waitVoice();
await t.uncallout("held");
const again = await menuOn(n.ctx, "Hair");
await t.callout("menu", again, "Push, wrap, move", 8);
await t.say("Push to library sends this node's changes back to the library item, for every workflow. Save to library, on a row the library doesn't have, adds it as a new item.");
await t.waitVoice();
await t.say("Wrap into new bundle turns rows into a bundle, and Move to top or bottom reorders them.");
await t.waitVoice();
await t.uncallout("menu");
await t.click(again.getByRole("menuitem", { name: "Release hold" }));
await t.beat(600);
await t.deselect();
if (!(await t.node(ctx2).isVisible())) throw new StepError("The second Context left the screen.");
});
await t.h.step("Outro", async () => {
await t.say("Order your rows like a recipe, chain Contexts for stages, and use the dots to spot what writes over what. The Context page in the docs lists every marker.");
await t.waitVoice();
});
},
);
+135
View File
@@ -0,0 +1,135 @@
// Nodes 2: WP Prompt Assembler in depth. A Context (Role, Hair, Setting) into an Assembler. Typing $
// opens the variable list; a name no node sets shows as unresolved and prints nothing; $$ prints a
// dollar sign. The coloured preview and the variable chips. Two Assemblers on one Context: a long
// and a short prompt from the same picks. The preview against the real prompt after a Run. A04
// already shows .0 / .GROUP / .neg and A10 the negative box. Prompt-only: no model.
import { StepError } from "watchwright";
import { tutorial } from "../tutorial.js";
tutorial(
"N02",
{
title: "WP Prompt Assembler in depth",
description: "Writing templates in the Assembler: the variable list, missing names, $$, the coloured preview, two prompts from one Context, and preview against run.",
learn: "Autocomplete, unresolved variables, $$, the preview and chips, several Assemblers per Context, preview vs the run's prompt.",
},
async (t) => {
const page = t.page;
const box = (asm: string) => t.node(asm).locator("textarea, [contenteditable='true']").first();
const preview = (asm: string) => t.node(asm).locator('[data-test="asm-preview"]');
const previewText = (asm: string) => preview(asm).innerText().catch(() => "");
const waitPreview = async (asm: string, re: RegExp, yes = true) => {
for (let i = 0; i < 20; i++) {
if (re.test(await previewText(asm)) === yes) return;
await t.beat(300);
}
throw new StepError(`The Assembler preview ${yes ? "never showed" : "still shows"} ${re}.`);
};
const n = await t.h.step("The variable list", async () => {
await t.go("/");
await t.speedUp();
const ctx = await t.addNode("WP Context", { x: 480, y: 170 });
await t.pickModules(ctx, ["Role", "Hair", "Setting"]);
const asm = await t.addNode("WP Prompt Assembler", { x: 1150, y: 170 });
await t.wire(ctx, "context", asm, "context");
await t.normalSpeed();
await t.say("The Assembler turns variables into a prompt. Click its template and type A painting of, then a dollar sign.");
await t.click(box(asm));
await t.type("A painting of $", { cps: 16 });
await t.beat(700);
await t.waitVoice();
const list = page.getByRole("listbox").last();
if (await list.isVisible().catch(() => false)) {
await t.callout("list", list, "Every variable upstream", 6);
await t.say("A dollar sign opens the list of every variable the nodes before it set. Keep typing to narrow it.");
await t.waitVoice();
await t.uncallout("list");
}
await t.type("role with $hair, $setting", { cps: 16 });
await t.press("Escape");
await waitPreview(asm, /^A painting of .+ with .+,/);
return { ctx, asm };
});
await t.h.step("The preview and the chips", async () => {
await t.callout("prev", preview(n.asm), "Each word in its variable's color", 6);
await t.say("The preview shows what this seed prints, each part in its variable's color. It updates as you type and as the seed changes.");
await t.waitVoice();
await t.uncallout("prev");
await t.say("Above it, a chip for each variable. Click a chip to put it in the template; right-click it for more.");
await t.waitVoice();
});
await t.h.step("A name nothing sets", async () => {
await t.say("Type a variable no node sets: dollar mood.");
await t.click(box(n.asm));
await t.press("ControlOrMeta+End");
await t.type(", $mood", { cps: 16 });
await t.press("Escape");
await t.beat(800);
await t.waitVoice();
const status = t.node(n.asm).locator('[data-test="asm-preview-status"]');
await t.callout("st", status, "1 unresolved", 5);
await t.say("The Assembler says it's unresolved, and marks the name. Run anyway and it prints nothing there; the Prompt Cleaner tidies the comma it leaves.");
await t.waitVoice();
await t.uncallout("st");
await t.click(box(n.asm));
await t.press("ControlOrMeta+End");
for (let i = 0; i < 7; i++) await t.press("Backspace");
await t.press("Escape");
await t.beat(500);
});
await t.h.step("A real dollar sign", async () => {
await t.say("For a real dollar sign, write two. Type: on a 5 dollar coin, with two dollar signs.");
await t.click(box(n.asm));
await t.press("ControlOrMeta+End");
await t.type(", on a $$5 coin", { cps: 16 });
await t.press("Escape");
await waitPreview(n.asm, /\$5 coin/);
await t.callout("prev", preview(n.asm), "One dollar sign", 5);
await t.waitVoice();
await t.say("Two dollar signs print one, and the 5 is just text.");
await t.waitVoice();
await t.uncallout("prev");
});
const asm2 = await t.h.step("Two prompts from one Context", async () => {
await t.say("One Context can feed several Assemblers. Add a second, for a short tag prompt.");
const a2 = await t.addNode("WP Prompt Assembler", { x: 1150, y: 640 });
await t.wire(n.ctx, "context", a2, "context");
await t.click(box(a2));
await t.type("$role, $setting", { cps: 16 });
await t.press("Escape");
await waitPreview(a2, /,/);
await t.waitVoice();
await t.callout("both", [preview(n.asm), preview(a2)], "Same picks, two prompts", 6);
await t.say("Both read the same picks: a long prompt for one model or sampler, a short one for another, or a caption to save beside the image. Each sends only the negatives of what it prints.");
await t.waitVoice();
await t.uncallout("both");
return a2;
});
await t.h.step("Preview and run", async () => {
await t.say("Wire the first Assembler's prompt into Preview as Text, and run.");
const text = await t.addNode("Preview as Text", { x: 1700, y: 170 });
await t.wire(n.asm, "prompt", text, "source");
await t.run();
await t.beat(1200);
await t.waitVoice();
await t.callout("cmp", [preview(n.asm), t.node(text)], "Preview, and the real prompt", 6);
await t.say("The run printed what the preview showed: it rolls with the Context's own seed. After the run, the seed moves on as its mode says, and the preview follows.");
await t.waitVoice();
await t.uncallout("cmp");
await t.say("A value that only exists at run time, like a caption wired into an Injector, shows by its name in the preview and as text after the run.");
await t.waitVoice();
if (!(await t.node(asm2).isVisible())) throw new StepError("The second Assembler left the screen.");
});
await t.h.step("Outro", async () => {
await t.say("Type a dollar sign for the list, watch the preview, and use as many Assemblers as you need prompts. The Assembler page in the docs has the full template grammar.");
await t.waitVoice();
});
},
);
+241
View File
@@ -0,0 +1,241 @@
// Nodes 7: WP Toggles in depth. Toggles → Context (Role) → Assembler, watched in the Assembler's
// preview (no run needed). A Lantern toggle with a Value, shown in the four styles (Switch, Check,
// Buttons with words of its own, Chip). Rain and Puddles, where Puddles needs Rain: Rain off takes
// Puddles off and Rain on brings back its choice. A label folds the weather toggles into a group.
// The other settings in a toggle's window: Upstream can set it, When off (Mute / Bypass, Invert,
// canvas groups) and the BOOLEAN output for switch nodes. A17 already shows a rule reading a toggle,
// Pick one, values and negatives, and When off with a Node Group. Prompt-only: no model.
import { StepError } from "watchwright";
import { tutorial } from "../tutorial.js";
tutorial(
"N07",
{
title: "WP Toggles in depth",
description: "Every part of WP Toggles: the four styles, toggles that need another, labels that group them, upstream presets, muting and bypassing, and BOOLEAN outputs.",
learn: "Switch / Check / Buttons / Chip, Needs, labels and folding, Upstream can set it, When off with Invert and canvas groups, BOOLEAN outputs.",
},
async (t) => {
const page = t.page;
const win = () => page.getByRole("dialog").filter({ has: page.locator('[data-test="tg-window-done"]') }).last();
const winField = (test: string) =>
win().locator(`[data-test="${test}"] input, input[data-test="${test}"], [data-test="${test}"] textarea, textarea[data-test="${test}"], [data-test="${test}"] [contenteditable="true"], [data-test="${test}"][contenteditable="true"]`).first();
const winDone = async () => {
await t.click(win().locator('[data-test="tg-window-done"]'));
await t.beat(500);
};
const row = (tg: string, name: string) => t.node(tg).locator('[data-test^="tg-row-"]').filter({ hasText: name }).first();
const ctl = (tg: string, name: string) => row(tg, name).locator('[data-test^="tg-ctl-"]').first();
const openEdit = async (tg: string) => {
const box = await t.node(tg).boundingBox();
if (box) await t.click({ x: box.x + box.width / 2, y: box.y + 14 });
await t.beat(500);
await t.click(page.locator(".wp-tb-icon--edit").first().locator("xpath=ancestor::button[1]"));
await t.beat(800);
};
const editDone = async (tg: string) => {
await t.click(t.node(tg).locator('[data-test="tg-edit-done"]'));
await t.beat(700);
await t.deselect();
};
// A new toggle (edit view open): its window opens; name it, write a variable, a value, and turn its globe off.
const addToggle = async (tg: string, name: string, v: string, value: string) => {
await t.click(t.node(tg).locator('[data-test="tg-add"]'));
await t.beat(700);
await t.fill(winField("tg-name"), name, { cps: 16 });
await t.press("Tab");
await t.fill(winField("tg-var"), v, { cps: 16 });
await t.press("Tab");
await t.fill(winField("tg-value"), value, { cps: 16 });
await t.beat(300);
};
const openRow = async (tg: string, name: string) => {
await t.click(row(tg, name).locator('[data-test^="tg-edit-"]').first());
await t.beat(700);
};
const preview = (asm: string) => t.node(asm).locator('[data-test="asm-preview"]');
const previewSays = async (asm: string, re: RegExp, yes: boolean) => {
for (let i = 0; i < 20; i++) {
if (re.test(await preview(asm).innerText().catch(() => "")) === yes) return;
await t.beat(300);
}
throw new StepError(`The Assembler preview ${yes ? "never showed" : "still shows"} ${re}.`);
};
const n = await t.h.step("A toggle with words", async () => {
await t.go("/");
await t.speedUp();
const tg = await t.addNode("WP Toggles", { x: 300, y: 170 });
const ctx = await t.addNode("WP Context", { x: 900, y: 170 });
await t.wire(tg, "context", ctx, "upstream");
await t.pickModules(ctx, ["Role"]);
const asm = await t.addNode("WP Prompt Assembler", { x: 1450, y: 170 });
await t.wire(ctx, "context", asm, "context");
await t.click(t.node(asm).locator("textarea, [contenteditable='true']").first());
await t.type("A painting of $role $lantern $rain $puddles");
await t.press("Escape");
await t.beat(600);
await t.normalSpeed();
await t.say("WP Toggles at the start of a chain, a Context, and an Assembler whose prompt reads three toggles we're about to make.");
await t.waitVoice();
await t.say("Select the Toggles and press the pencil. Add a toggle called Lantern, with the words holding a lantern.");
await openEdit(tg);
await addToggle(tg, "Lantern", "lantern", "holding a lantern");
await t.waitVoice();
return { tg, ctx, asm };
});
await t.h.step("Four styles", async () => {
const styles = win().locator('[data-test^="tg-style-"]');
await t.callout("styles", styles.first().locator(".."), "How it looks on the node", 5);
await t.say("Style is how the toggle looks on the node. Switch is the default.");
await t.waitVoice();
await t.uncallout("styles");
await t.say("Check is a tick box.");
await t.click(win().locator('[data-test="tg-style-checkbox"]'));
await t.beat(800);
await t.waitVoice();
await t.say("Buttons shows two words of your own. Write With and Without.");
await t.click(win().locator('[data-test="tg-style-buttons"]'));
await t.beat(500);
await t.fill(win().locator('[data-test="tg-on-label"] input, input[data-test="tg-on-label"]').first(), "With", { cps: 14 });
await t.press("Tab");
await t.fill(win().locator('[data-test="tg-off-label"] input, input[data-test="tg-off-label"]').first(), "Without", { cps: 14 });
await t.press("Tab");
await t.beat(500);
await t.waitVoice();
await t.callout("prev", win().locator('[data-test="tg-window-preview"]'), "Its look on the node", 5);
await t.say("The window shows how it'll look.");
await t.waitVoice();
await t.uncallout("prev");
await t.say("And Chip is a small pill that lights up while it's on. Keep the buttons.");
await t.waitVoice();
await winDone();
const globe = row(n.tg, "Lantern").locator('[data-test^="tg-internal-"]').first();
await t.say("Turn off its globe so the prompt can print it, and press Done.");
await t.click(globe);
await t.beat(400);
await editDone(n.tg);
await t.waitVoice();
await t.say("Click With.");
await t.click(row(n.tg, "Lantern").getByText("With", { exact: true }).first());
await previewSays(n.asm, /holding a lantern/, true);
await t.callout("prev", preview(n.asm), "Lantern's words", 5);
await t.say("Its words are in the prompt. Click Without, and they're gone.");
await t.waitVoice();
await t.uncallout("prev");
await t.click(row(n.tg, "Lantern").getByText("Without", { exact: true }).first());
await previewSays(n.asm, /holding a lantern/, false);
});
await t.h.step("One toggle needs another", async () => {
await t.say("Now two toggles that belong together: Rain, and Puddles, which only makes sense in the rain.");
await openEdit(n.tg);
await addToggle(n.tg, "Rain", "rain", "in the rain");
await winDone();
await addToggle(n.tg, "Puddles", "puddles", "with puddles on the ground");
await t.waitVoice();
const needs = win().locator('[data-test="tg-needs"]');
await t.callout("needs", needs, "Only on while Rain is on", 5);
await t.say("Needs makes it depend on another toggle. Pick Rain.");
await t.click(needs);
await t.beat(400);
await t.click(page.getByRole("option", { name: /^Rain\b/ }).first());
await t.beat(500);
await t.waitVoice();
await t.uncallout("needs");
await winDone();
for (const name of ["Rain", "Puddles"]) {
await t.click(row(n.tg, name).locator('[data-test^="tg-internal-"]').first());
await t.beat(300);
}
await editDone(n.tg);
await t.say("Switch both on.");
await t.click(ctl(n.tg, "Rain"));
await t.beat(400);
await t.click(ctl(n.tg, "Puddles"));
await previewSays(n.asm, /with puddles/, true);
await t.waitVoice();
await t.say("Now switch Rain off. Puddles goes off with it, and the prompt drops both.");
await t.click(ctl(n.tg, "Rain"));
await previewSays(n.asm, /with puddles/, false);
await t.callout("p", row(n.tg, "Puddles"), "Off, because Rain is off", 4);
await t.waitVoice();
await t.uncallout("p");
await t.say("Switch Rain back on. Puddles remembers it was on, and comes back.");
await t.click(ctl(n.tg, "Rain"));
await previewSays(n.asm, /with puddles/, true);
await t.waitVoice();
});
await t.h.step("Labels group toggles", async () => {
await t.say("A label groups toggles under a heading. In edit view, Add label, and call it Weather.");
await openEdit(n.tg);
await t.click(t.node(n.tg).locator('[data-test="tg-add-label"]'));
await t.beat(600);
await t.type("Weather", { cps: 16 });
await t.press("Enter");
await t.beat(500);
await t.waitVoice();
await t.say("Drag it above Rain with its grip. Everything under a label belongs to it, until the next label.");
const label = t.node(n.tg).locator('[data-test^="tg-label-"]').filter({ hasText: "Weather" }).first();
const lb = await label.boundingBox();
const rb = await row(n.tg, "Rain").boundingBox();
if (lb && rb) await t.drag({ x: lb.x + 10, y: lb.y + lb.height / 2 }, { x: rb.x + 10, y: rb.y + 4 });
await t.beat(700);
await t.waitVoice();
await editDone(n.tg);
const fold = t.node(n.tg).locator('[data-test^="tg-fold-"]').first();
await t.callout("fold", fold, "Fold the group", 4);
await t.say("Fold it to keep a long node short. Folded toggles keep working.");
await t.click(fold);
await t.beat(700);
await t.waitVoice();
await t.uncallout("fold");
await previewSays(n.asm, /in the rain/, true);
await t.click(fold);
await t.beat(500);
});
await t.h.step("The rest of a toggle's window", async () => {
await openEdit(n.tg);
await openRow(n.tg, "Lantern");
const up = win().locator('[data-test="tg-upstream"]');
await t.callout("up", up, "A preset can set it", 5);
await t.say("Upstream can set it lets a Context or Injector before this node decide the switch, with a yes or no word in the same variable. That's how one preset switches many toggles.");
await t.waitVoice();
await t.uncallout("up");
const control = win().locator('[data-test^="tg-control-"]').first().locator("..");
await t.callout("ctl", control, "Also mute or bypass nodes", 5);
await t.say("When off can also mute or bypass other nodes: wire them into the dot in front of the row, or pick canvas groups by name, with no wires.");
await t.click(win().locator('[data-test="tg-control-bypass"]'));
await t.beat(600);
await t.waitVoice();
await t.uncallout("ctl");
await t.callout("inv", win().locator('[data-test="tg-invert"]'), "Bypass while on instead", 4);
await t.say("Invert turns it around: bypass while on, for a branch you only want when the switch is off.");
await t.waitVoice();
await t.uncallout("inv");
await t.click(win().locator('[data-test="tg-control-"]'));
await t.beat(400);
const pin = win().locator('[data-test="tg-pin"]');
await t.callout("pin", pin, "A BOOLEAN socket", 4);
await t.say("BOOLEAN output gives the toggle its own socket on the node, for any switch node from ComfyUI or another pack.");
await t.click(pin);
await t.beat(500);
await t.waitVoice();
await t.uncallout("pin");
await winDone();
await editDone(n.tg);
await t.callout("out", t.node(n.tg), "Lantern's output", 4);
await t.say("The node now has a Lantern output, true or false.");
await t.waitVoice();
await t.uncallout("out");
});
await t.h.step("Outro", async () => {
await t.say("Toggles are your workflow's switchboard: words for the prompt, switches for rules, and a way to turn whole branches off. The WP Toggles page in the docs lists every setting.");
await t.waitVoice();
});
},
);