feat(autocomplete): apply keep-underscores setting to insertion

Rename `autocomplete_replace_underscores` to `autocomplete_keep_underscores`
(default false) so both autocomplete toggles read as "keep" semantics.
`insertTag` now honors the setting for both tag and artist modes, matching
the dropdown display. Embedding insertion always preserves underscores
because the names are filesystem identifiers.

- ui_definitions: rename option, flip default, update label
- scripts/autocomplete: rename handler, checkbox, and config-bridge keys
- javascript/autocomplete: invert render polarity; insertTag takes a `kind`
  parameter; drop the artist-mode hardcoded underscore replacement so the
  setting controls tags and artists uniformly

Closes #4827
This commit is contained in:
CalamitousFelicitousness
2026-05-10 19:47:54 +01:00
parent df15857a61
commit f2227d7785
3 changed files with 24 additions and 22 deletions
+13 -11
View File
@@ -358,7 +358,7 @@ function insertExtraNetwork(textarea, item, kind) {
}
/** Insert a tag at the current word position, replacing the typed prefix. */
function insertTag(textarea, tagName) {
function insertTag(textarea, tagName, kind = 'tag') {
const info = getCurrentWord(textarea);
if (!info || (info.mode !== 'tag' && info.mode !== 'artist')) return;
const { value } = textarea;
@@ -371,13 +371,13 @@ function insertTag(textarea, tagName) {
const prefix = needsSepBefore ? `${sep} ` : '';
let suffix = `${sep} `;
if (after.length > 0 && after.trimStart().startsWith(',')) suffix = ' ';
// Artist mode: optionally keep the `@` prefix (Anima syntax); always convert underscores to spaces
// since Anima requires space-separated artist names. The `@` is consumed for non-Anima models.
// Embedding names are file-system identifiers, so underscores must be preserved regardless of the user setting.
// Tags and artists honor `autocomplete_keep_underscores`; default is to swap `_` for space.
const keepUnderscores = window.opts?.autocomplete_keep_underscores ?? false;
let body = tagName;
if (info.mode === 'artist') {
body = body.replace(/_/g, ' ');
if (window.opts?.autocomplete_at_prefix_artist) body = `@${body}`;
}
if (kind !== 'embed' && !keepUnderscores) body = body.replace(/_/g, ' ');
// Artist mode optionally keeps the `@` prefix (Anima syntax). The `@` is consumed for non-Anima models.
if (info.mode === 'artist' && window.opts?.autocomplete_at_prefix_artist) body = `@${body}`;
const insertion = `${prefix}${escapeParensForPrompt(body)}${suffix}`;
textarea.value = before.trimEnd() + (before.trimEnd().length > 0 ? ' ' : '') + insertion + after.trimStart();
// Position cursor after the inserted tag + separator
@@ -447,7 +447,7 @@ const dropdown = {
},
render() {
const replaceUnderscores = window.opts?.autocomplete_replace_underscores ?? true;
const keepUnderscores = window.opts?.autocomplete_keep_underscores ?? false;
const queryNorm = this.query.toLowerCase().replace(/ /g, '_');
this.listEl.replaceChildren();
this.results.forEach((tag, i) => {
@@ -462,7 +462,9 @@ const dropdown = {
dot.title = kind === 'tag' ? (engine.categoryNames[tag.category] || '') : kind;
const name = document.createElement('span');
name.className = 'autocomplete-tag';
const tagText = replaceUnderscores ? tag.display.replace(/_/g, ' ') : tag.display;
// Embeddings are file-name identifiers, so they always render as-is to match how they get inserted.
const swapForKind = kind !== 'embed';
const tagText = (swapForKind && !keepUnderscores) ? tag.display.replace(/_/g, ' ') : tag.display;
const canonicalMatch = tag.name.indexOf(queryNorm);
if (canonicalMatch >= 0 && queryNorm.length > 0) {
const mark = document.createElement('mark');
@@ -480,7 +482,7 @@ const dropdown = {
if (tag.matchedVia === 'alias') annotationTerm = tag.matchedAlias;
else if (tag.matchedVia === 'translation') annotationTerm = tag.matchedTerm;
if (annotationTerm) {
const annotationDisplay = replaceUnderscores ? annotationTerm.replace(/_/g, ' ') : annotationTerm;
const annotationDisplay = (swapForKind && !keepUnderscores) ? annotationTerm.replace(/_/g, ' ') : annotationTerm;
const annotationLower = annotationTerm.toLowerCase();
const annotationMatch = annotationLower.indexOf(queryNorm);
const prefix = tag.matchedVia === 'translation' ? ' \u{1F310} ' : ' (';
@@ -561,7 +563,7 @@ const dropdown = {
insertExtraNetwork(this.textarea, result, result.kind);
} else {
// 'embed' kind and untagged tag results both go through insertTag (comma-aware, paren-escaped).
insertTag(this.textarea, result.display ?? result.name);
insertTag(this.textarea, result.display ?? result.name, result.kind);
}
}
this.hide();
+1 -1
View File
@@ -684,7 +684,7 @@ def create_settings(cmd_opts):
"autocomplete_active": OptionInfo(False, "Enable Autocomplete", gr.Checkbox, {"visible": False}),
"autocomplete_enabled": OptionInfo([], "Enabled tag autocomplete files", gr.Dropdown, {"multiselect": True, "choices": [], "visible": False}),
"autocomplete_min_chars": OptionInfo(3, "Min autocomplete chars", gr.Slider, {"minimum": 2, "maximum": 6, "step": 1, "visible": False}),
"autocomplete_replace_underscores": OptionInfo(True, "Replace underscores in autocomplete", gr.Checkbox, {"visible": False}),
"autocomplete_keep_underscores": OptionInfo(False, "Keep underscores in autocomplete", gr.Checkbox, {"visible": False}),
"autocomplete_append_comma": OptionInfo(True, "Append comma after autocomplete", gr.Checkbox, {"visible": False}),
# Caption settings (controlled via Caption Tab UI)
+10 -10
View File
@@ -27,7 +27,7 @@ def get_config_json():
"autocomplete_active": bool(shared.opts.data.get('autocomplete_active', False)),
"autocomplete_enabled": enabled,
"autocomplete_min_chars": shared.opts.data.get('autocomplete_min_chars', 3),
"autocomplete_replace_underscores": shared.opts.data.get('autocomplete_replace_underscores', True),
"autocomplete_keep_underscores": shared.opts.data.get('autocomplete_keep_underscores', False),
"autocomplete_append_comma": shared.opts.data.get('autocomplete_append_comma', True),
"autocomplete_at_prefix_artist": shared.opts.data.get('autocomplete_at_prefix_artist', False),
"autocomplete_translations": bool(shared.opts.data.get('autocomplete_translations', False)),
@@ -52,8 +52,8 @@ def on_min_chars_change(value):
return get_config_json()
def on_replace_underscores_change(value):
shared.opts.data['autocomplete_replace_underscores'] = bool(value)
def on_keep_underscores_change(value):
shared.opts.data['autocomplete_keep_underscores'] = bool(value)
shared.opts.save(silent=True)
return get_config_json()
@@ -180,10 +180,10 @@ class AutocompleteScript(scripts_manager.Script):
refresh_btn = ToolButton(value=symbols.refresh, elem_id=self.elem_id("refresh"))
update_btn = ToolButton(value=symbols.save, elem_id=self.elem_id("update"))
with gr.Row():
replace_underscores = gr.Checkbox(
label="Replace underscores",
value=shared.opts.data.get('autocomplete_replace_underscores', True),
elem_id=self.elem_id("replace_underscores"),
keep_underscores = gr.Checkbox(
label="Keep underscores",
value=shared.opts.data.get('autocomplete_keep_underscores', False),
elem_id=self.elem_id("keep_underscores"),
)
append_comma = gr.Checkbox(
label="Comma separator",
@@ -217,14 +217,14 @@ class AutocompleteScript(scripts_manager.Script):
active_cb.change(fn=on_active_change, inputs=[active_cb], outputs=[config_json, status])
enabled_dd.change(fn=on_enabled_change, inputs=[enabled_dd], outputs=[config_json, status])
min_chars.change(fn=on_min_chars_change, inputs=[min_chars], outputs=[config_json])
replace_underscores.change(fn=on_replace_underscores_change, inputs=[replace_underscores], outputs=[config_json])
keep_underscores.change(fn=on_keep_underscores_change, inputs=[keep_underscores], outputs=[config_json])
append_comma.change(fn=on_append_comma_change, inputs=[append_comma], outputs=[config_json])
at_prefix_artist.change(fn=on_at_prefix_artist_change, inputs=[at_prefix_artist], outputs=[config_json])
translations_cb.change(fn=on_translations_change, inputs=[translations_cb], outputs=[config_json])
refresh_btn.click(fn=on_refresh, inputs=[], outputs=[enabled_dd, status])
update_btn.click(fn=on_update, inputs=[enabled_dd], outputs=[status])
for comp in [enabled_dd, min_chars, replace_underscores, append_comma, at_prefix_artist, translations_cb, config_json, status]:
for comp in [enabled_dd, min_chars, keep_underscores, append_comma, at_prefix_artist, translations_cb, config_json, status]:
comp.do_not_save_to_config = True
return [active_cb, enabled_dd, min_chars, replace_underscores, append_comma, at_prefix_artist, translations_cb, config_json]
return [active_cb, enabled_dd, min_chars, keep_underscores, append_comma, at_prefix_artist, translations_cb, config_json]