Defaults an administrator can actually set

There were none. `workflow.DEFAULTS` was the only source, so 512x512, euler and
twenty steps were what every instance got whatever card it was running on -- and
512 square on an SDXL checkpoint is precisely what the tool's own description
warns produces duplicated limbs. The two ways round it were both bad: bake
literals into a template where the placeholders should be, or write prose in the
instructions box and hope.

Three rungs now, most specific winning, with DEFAULTS staying underneath as the
floor so an instance that sets nothing behaves exactly as it did and a floor
improved in code still reaches everybody. An empty box is "no opinion" rather
than zero, which matters: read as a number it would set every instance to zero
steps, and ComfyUI refuses that in a way that looks like a broken model.

The right control for each, because a text box is wrong for most of them. The
samplers and schedulers were already being discovered by the Test button, stored,
and read by nothing at all -- they are the pickers now. A stored value missing
from the list is kept as an option anyway, or opening this page and pressing Save
would silently clear a working setting. Checkpoints are chosen rather than typed,
and the instance default is a rung of its own instead of "whatever happens to be
first in a textarea somebody filled in some order".

And batch, at last: `batch_size` was a literal 1 in the base template, so an
administrator whose card can comfortably make four had no way of saying so.
Deliberately not something a model may set -- one asking for six because it is
unsure is the exact cost this must not invite.

The tool's schema restates the defaults it quotes. Every "Default 20." in there
was written when there was one set of defaults in the world; left alone, an
instance drawing at 1024 would go on telling the model 512, and the model reasons
from that sentence rather than ignoring it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-06 11:50:37 +02:00
co-authored by Claude Opus 5
parent 54ed030732
commit 0fa05c88b2
13 changed files with 721 additions and 31 deletions
+18
View File
@@ -203,6 +203,24 @@ a.tabs__tab { text-decoration: none; }
flex-wrap: wrap;
}
/*
Two or three short fields on one line.
Numbers with their own units — width and height, steps and cfg and denoise —
read as a set and take a fraction of the width each, so stacking them makes a
card that is mostly whitespace and a form that is mostly scrolling. Auto-fit
rather than a fixed count: the same markup gives three columns on a wide
screen and one on a narrow one with no breakpoint, which matters because
`chat.css` has a test refusing media queries and this file should not drift
from that habit without a reason.
*/
.field-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: var(--sp-3);
}
.field-row > .field { margin-bottom: var(--sp-4); }
/* Legacy aliases so existing admin templates keep their spacing. */
.form-grid { display: block; }
.connection__head { display: flex; align-items: center; justify-content: space-between;
+26
View File
@@ -759,6 +759,32 @@
document.addEventListener("htmx:afterSettle", scan);
})();
/*
Two number fields set together.
Image sizes come in pairs and nobody types 1024 and then 1536; they pick
"portrait". A button rather than a select of pairs, because a select would
have to enumerate every combination somebody might want and these are only
the common ones — the two boxes are still there and still authoritative.
Delegated and keyed on the attributes rather than on the image page's ids, so
the next screen with a paired number wants no new code.
*/
document.addEventListener("click", function (event) {
var preset = event.target.closest("[data-width][data-height]");
if (!preset) return;
var row = preset.closest("[data-size-presets]");
if (!row) return;
event.preventDefault();
var form = preset.closest("form");
if (!form) return;
var width = form.querySelector('[name="default_width"]');
var height = form.querySelector('[name="default_height"]');
if (width) width.value = preset.dataset.width;
if (height) height.value = preset.dataset.height;
});
/*
A toast asked for by the server.
+137
View File
@@ -100,6 +100,143 @@
<div id="images-test-result"></div>
</section>
<section class="card">
<h2 class="card__title">What a generation uses by default</h2>
<p class="card__lede">
What every picture is drawn with unless the model names otherwise. Leave a
box empty to use the built-in value shown beside it — the built-ins are
SD1.5-era, and 512×512 on an SDXL checkpoint is what produces the
duplicated limbs.
</p>
<div class="field">
<label class="field__label" for="default_checkpoint">Checkpoint</label>
{# A select rather than a box, from the list above. Typing a name that is
not there produced a picture drawn with whatever happened to be first,
with nothing anywhere saying so. #}
<select class="select" id="default_checkpoint" name="default_checkpoint">
<option value="">The first in the list above</option>
{% for name in values.checkpoints or [] %}
<option value="{{ name }}" {{ 'selected' if values.default_checkpoint == name }}>
{{ name }}
</option>
{% endfor %}
</select>
</div>
<div class="field-row">
<div class="field">
<label class="field__label" for="default_width">Width</label>
<input class="input" type="number" id="default_width" name="default_width"
min="64" max="2048" step="64" placeholder="512"
value="{{ values.default_width }}">
</div>
<div class="field">
<label class="field__label" for="default_height">Height</label>
<input class="input" type="number" id="default_height" name="default_height"
min="64" max="2048" step="64" placeholder="512"
value="{{ values.default_height }}">
</div>
</div>
{# The five sizes anybody actually picks. Buttons rather than a select
because they set two fields at once, and a select of pairs would have to
name every combination somebody might want. #}
<div class="btn-row" data-size-presets>
{% for label, w, h in [
("512²", 512, 512), ("768²", 768, 768), ("1024²", 1024, 1024),
("1024×1536", 1024, 1536), ("1536×1024", 1536, 1024)
] %}
<button class="btn btn--sm" type="button"
data-width="{{ w }}" data-height="{{ h }}">{{ label }}</button>
{% endfor %}
</div>
<div class="field-row">
<div class="field">
<label class="field__label" for="default_steps">Steps</label>
<input class="input" type="number" id="default_steps" name="default_steps"
min="1" max="150" step="1" placeholder="20"
value="{{ values.default_steps }}">
</div>
<div class="field">
<label class="field__label" for="default_cfg">Guidance (cfg)</label>
<input class="input" type="number" id="default_cfg" name="default_cfg"
min="0" max="30" step="0.5" placeholder="8"
value="{{ values.default_cfg }}">
</div>
<div class="field">
<label class="field__label" for="default_denoise">Denoise</label>
<input class="input" type="number" id="default_denoise" name="default_denoise"
min="0" max="1" step="0.05" placeholder="1"
value="{{ values.default_denoise }}">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field__label" for="default_sampler">Sampler</label>
{# From what the Test button read off ComfyUI. There are forty-odd and
spelling one wrong is a refused workflow, so it is picked rather than
typed — and the current value is kept as an option even when the list
has not been read, or saving this page would quietly clear it. #}
<select class="select" id="default_sampler" name="default_sampler">
<option value="">euler (built-in)</option>
{% for name in values.samplers or [] %}
<option value="{{ name }}" {{ 'selected' if values.default_sampler == name }}>
{{ name }}
</option>
{% endfor %}
{% if values.default_sampler and values.default_sampler not in (values.samplers or []) %}
<option value="{{ values.default_sampler }}" selected>
{{ values.default_sampler }} (not in this ComfyUI's list)
</option>
{% endif %}
</select>
</div>
<div class="field">
<label class="field__label" for="default_scheduler">Scheduler</label>
<select class="select" id="default_scheduler" name="default_scheduler">
<option value="">normal (built-in)</option>
{% for name in values.schedulers or [] %}
<option value="{{ name }}" {{ 'selected' if values.default_scheduler == name }}>
{{ name }}
</option>
{% endfor %}
{% if values.default_scheduler
and values.default_scheduler not in (values.schedulers or []) %}
<option value="{{ values.default_scheduler }}" selected>
{{ values.default_scheduler }} (not in this ComfyUI's list)
</option>
{% endif %}
</select>
</div>
<div class="field">
<label class="field__label" for="default_batch">Images per run</label>
<input class="input" type="number" id="default_batch" name="default_batch"
min="1" max="8" step="1" placeholder="1"
value="{{ values.default_batch }}">
</div>
</div>
{% if not values.samplers %}
<p class="field__hint">
Press <strong>Test &amp; read what it has</strong> above to fill the sampler
and scheduler lists from your ComfyUI.
</p>
{% endif %}
<div class="field">
<label class="field__label" for="default_negative">Negative prompt</label>
<input class="input" type="text" id="default_negative" name="default_negative"
placeholder="text, watermark" value="{{ values.default_negative }}">
<p class="field__hint">
Used when the model does not write one of its own. It writes one often,
so this is a floor rather than something always applied — “always add
these words” belongs in the instructions below, where the model is told
to include them.
</p>
</div>
</section>
<section class="card">
<h2 class="card__title">Checking the result</h2>
<div class="field">
@@ -75,15 +75,46 @@
<code>"{{ '{{prompt}}' }}, masterpiece"</code> works. Anything you leave out
takes its default.
</p>
<p class="field__hint">
Available:
{% for name in placeholders %}<code>{{ '{{' ~ name ~ '}}' }}</code>{{ ", " if not loop.last }}{% endfor %}.
<code>{{ '{{prompt}}' }}</code> is required — without it every image would
be the same.
</p>
</div>
</section>
<section class="card">
<h2 class="card__title">The placeholders</h2>
<p class="card__lede">
Every hole a template may carry, what it fills, and what it resolves to
right now. <code>{{ '{{prompt}}' }}</code> is required — without it every
image would be the same one, whatever anybody typed.
</p>
{# The current value beside each name, the way /admin/prompts shows a
variable's. A legend that lists names and not values answers "what may I
write" and not "what will happen", and the second is the question
somebody has while looking at a workflow that came out wrong. #}
<div class="ref-list">
{% for name, kind, what, current in placeholder_help %}
<div class="ref-row">
<code>{{ '{{' ~ name ~ '}}' }}</code>
<span>
{{ what }}
{% if name == "prompt" %}<strong>Required.</strong>{% endif %}
</span>
<code class="faint">{{ current }}</code>
</div>
{% endfor %}
</div>
<p class="field__hint">
Two of these are not spelled the way ComfyUI spells them, which is the
mistake that costs an afternoon: <code>{{ '{{model}}' }}</code> fills
<code>ckpt_name</code> and <code>{{ '{{sampler}}' }}</code> fills
<code>sampler_name</code>.
</p>
<p class="field__hint">
The right-hand column is what an omitted placeholder resolves to today —
the instance defaults from the image settings page, falling back to the
built-in floor. A model may override any of them except
<code>{{ '{{batch}}' }}</code>, which is yours alone.
</p>
</section>
<div class="btn-row">
<button class="btn btn--primary" type="submit">
{{ "Add workflow" if is_new else "Save changes" }}