feat(autocomplete): move settings to always-on script UI

- create scripts/autocomplete.py as always-on script with dictionary
  management UI (dropdown, refresh, update, settings controls)
- move autocomplete settings from Settings > Extra Networks to hidden
  options, controlled via script UI with JS config bridge
- remove early exit in autocomplete.js when no dictionaries enabled,
  allowing dictionaries enabled via script UI to work without reload
- add id-based suffix matching to setHints.js for unique button hints
- add locale_en.json entries with tooltips for all autocomplete controls
This commit is contained in:
CalamitousFelicitousness
2026-04-09 02:00:45 +01:00
parent aa34db2c50
commit 3195bd6287
5 changed files with 236 additions and 35 deletions
+33 -10
View File
@@ -457,15 +457,39 @@ const PROMPT_IDS = [
'video_prompt', 'video_neg_prompt',
];
// -- Config bridge --
/** Monkey-patch script config bridge textboxes to push autocomplete config changes to window.opts immediately. */
function patchConfigBridge() {
const elements = gradioApp().querySelectorAll('[id$="_tag_autocomplete_config_json"]');
for (const el of elements) {
const textarea = el.querySelector('textarea');
if (!textarea || textarea.acBridgePatched) continue;
textarea.acBridgePatched = true;
const proto = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
Object.defineProperty(textarea, 'value', {
set(newValue) {
const oldValue = proto.get.call(textarea);
proto.set.call(textarea, newValue);
if (oldValue !== newValue && newValue) {
try {
const cfg = JSON.parse(newValue);
for (const [key, val] of Object.entries(cfg)) window.opts[key] = val;
log('autocomplete', 'config updated via bridge');
executeCallbacks(optionsChangedCallbacks);
} catch { /* ignore parse errors */ }
}
},
get() { return proto.get.call(textarea); },
});
}
}
// -- Initialization --
async function initAutocomplete() {
const enabled = window.opts?.autocomplete_enabled || [];
if (!enabled.length) {
log('autocomplete', 'no dictionaries enabled');
return;
}
log('autocomplete', `init: ${enabled.join(', ')}`);
log('autocomplete', enabled.length ? `init: ${enabled.join(', ')}` : 'init: no dictionaries enabled yet');
// Inject styles (CSS files in javascript/ are not auto-loaded)
const style = document.createElement('style');
style.textContent = [
@@ -489,11 +513,7 @@ async function initAutocomplete() {
document.head.appendChild(style);
dropdown.init();
await engine.loadEnabled();
if (engine.indices.size === 0) {
log('autocomplete', 'no dictionaries loaded');
return;
}
// Attach to all prompt textareas
// Attach to all prompt textareas; even if no dictionaries loaded yet, they may be enabled later via script UI
let attached = 0;
PROMPT_IDS.forEach((id) => {
const textarea = gradioApp().querySelector(`#${id} > label > textarea`);
@@ -513,4 +533,7 @@ async function initAutocomplete() {
await engine.loadEnabled();
}
});
// Watch for config updates from the script UI bridge
patchConfigBridge();
onAfterUiUpdate(() => patchConfigBridge());
}
+11 -5
View File
@@ -324,8 +324,11 @@ async function setHints() {
for (const el of elements) {
// localize elements text
let found;
if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim());
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
if (el.id) found = localeData.data.find((l) => l.id && (l.id === el.id || el.id.endsWith(l.id))); // prefer id match for disambiguation
if (!found) {
if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim());
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
}
if (found?.localized?.length > 0) {
if (!el.dataset.original) el.dataset.original = el.textContent;
replaceTextContent(el, found.localized);
@@ -359,9 +362,12 @@ async function applyHintToElement(el) {
|| (el.tagName === 'SPAN' && (el.parentElement?.tagName === 'LABEL' || el.parentElement?.classList.contains('label-wrap')));
if (!isValidElement) return;
let found; // find matching hint data
if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim());
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
let found; // find matching hint data - prefer id match for disambiguation
if (el.id) found = localeData.data.find((l) => l.id && (l.id === el.id || el.id.endsWith(l.id)));
if (!found) {
if (el.dataset.original) found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.dataset.original.toLowerCase().trim());
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
}
if (found?.localized?.length > 0) { // apply localization if found
if (!el.dataset.original) el.dataset.original = el.textContent;