186 lines
9.2 KiB
JavaScript
186 lines
9.2 KiB
JavaScript
import { createButton, createField, createScope, createSwitch, mountTheme } from '../ui/core.js';
|
|
import { createDialog } from '../ui/dialog.js';
|
|
import { MAX_TAG_BYTES } from '../autocomplete/tags.js';
|
|
import { parseTagFile } from '../autocomplete/tag-storage.js';
|
|
|
|
export function openAutocompleteSettings(autocomplete, settings, fallbackFocus) {
|
|
const existing = autocomplete.settingsDialog;
|
|
if (existing && !existing.scope.signal.aborted) {
|
|
existing.element.querySelector('input').focus();
|
|
return existing;
|
|
}
|
|
autocomplete.hideSuggestions();
|
|
const dialog = createDialog({ title: 'Embedding Autocomplete Settings', settings, fallbackFocus });
|
|
autocomplete.settingsDialog = dialog;
|
|
dialog.element.classList.add('ca-autocomplete-settings');
|
|
const form = document.createElement('form');
|
|
form.id = `${dialog.element.id}-form`;
|
|
const fields = new Map();
|
|
const group = (title, parent = form) => {
|
|
const fieldset = document.createElement('fieldset');
|
|
const legend = document.createElement('legend');
|
|
legend.textContent = title;
|
|
fieldset.append(legend);
|
|
parent.append(fieldset);
|
|
return fieldset;
|
|
};
|
|
const addSwitch = (parent, key, label) => {
|
|
const field = createSwitch(label, '', `${form.id}-${key}`);
|
|
const input = field.querySelector('input');
|
|
input.checked = autocomplete.settings[key] !== false;
|
|
fields.set(key, input);
|
|
parent.append(field);
|
|
};
|
|
const addInput = (parent, key, label, type = 'text', limits = {}) => {
|
|
const input = document.createElement('input');
|
|
Object.assign(input, { type, value: autocomplete.settings[key] ?? '', ...limits });
|
|
fields.set(key, input);
|
|
parent.append(createField(label, input, `${form.id}-${key}`));
|
|
return input;
|
|
};
|
|
const sources = group('Sources');
|
|
for (const [key, label] of [['enabled', 'Enable Autocomplete'], ['showEmbeddings', 'Show Embeddings'], ['showLoras', 'Show LoRAs'], ['showCustomWords', 'Show Custom Words']]) addSwitch(sources, key, label);
|
|
const tags = group('Tags and custom words');
|
|
const count = document.createElement('p');
|
|
count.className = 'ca-dialog-hint';
|
|
count.textContent = `${autocomplete.customWords.length.toLocaleString()} tags loaded`;
|
|
const hint = document.createElement('p');
|
|
hint.className = 'ca-dialog-hint';
|
|
hint.textContent = 'Import CSV or TXT, or load a URL. Lists replace the current tags when you save. Supports tag,frequency and tag,category,post_count,aliases. Maximum 32 MB.';
|
|
const pack = document.createElement('a');
|
|
pack.href = 'https://github.com/ComfyAssets/ComfyUI-KikoTools/releases/tag/tags-2026-10-16';
|
|
pack.target = '_blank';
|
|
pack.rel = 'noopener noreferrer';
|
|
pack.textContent = 'Download the Danbooru tag pack';
|
|
const source = document.createElement('p');
|
|
source.className = 'ca-dialog-hint ca-tag-source';
|
|
source.textContent = `Current source: ${(autocomplete.settings.tagSource || autocomplete.settings.customWordsUrl || 'None').replace(/^file:/, '')}`;
|
|
tags.append(count, source, hint, pack);
|
|
const url = addInput(tags, 'customWordsUrl', 'Tag list URL', 'url');
|
|
const load = createButton('Load URL');
|
|
const file = document.createElement('input');
|
|
file.type = 'file';
|
|
file.accept = '.csv,.txt,text/csv,text/plain';
|
|
tags.append(load, createField('Tag list file', file, `${form.id}-file`));
|
|
const advanced = document.createElement('details');
|
|
const summary = document.createElement('summary');
|
|
summary.textContent = 'Triggers, matching and insertion';
|
|
advanced.append(summary);
|
|
form.append(advanced);
|
|
const matching = group('Triggers and matching', advanced);
|
|
addInput(matching, 'embeddingTrigger', 'Embedding trigger');
|
|
addInput(matching, 'loraTrigger', 'LoRA trigger');
|
|
addInput(matching, 'quickTrigger', 'Quick embedding trigger');
|
|
addInput(matching, 'minChars', 'Minimum characters', 'number', { min: 1, max: 10, step: 1, required: true });
|
|
addInput(matching, 'maxSuggestions', 'Maximum suggestions', 'number', { min: 1, max: 100, step: 1, required: true });
|
|
addSwitch(matching, 'sortByDirectory', 'Group by directory');
|
|
const insertion = group('Insertion', advanced);
|
|
for (const [key, label] of [['autoInsertComma', 'Auto-insert comma'], ['replaceUnderscores', 'Replace underscores with spaces'], ['insertOnTab', 'Insert on Tab'], ['insertOnEnter', 'Insert on Enter']]) addSwitch(insertion, key, label);
|
|
const status = document.createElement('p');
|
|
status.role = 'status';
|
|
status.className = 'ca-tag-status';
|
|
status.textContent = autocomplete.customWordsError || '';
|
|
tags.append(status);
|
|
dialog.body.append(form);
|
|
const cancel = createButton('Cancel');
|
|
const save = createButton('Save Settings', 'ca-primary');
|
|
save.type = 'submit';
|
|
save.setAttribute('form', form.id);
|
|
dialog.actions.append(cancel, save);
|
|
let pending, busy = false;
|
|
const prepare = async callback => {
|
|
busy = true;
|
|
load.disabled = file.disabled = save.disabled = true;
|
|
status.dataset.error = 'false';
|
|
status.textContent = 'Loading tags…';
|
|
try {
|
|
const result = await callback();
|
|
if (dialog.scope.signal.aborted) return;
|
|
pending = result;
|
|
status.textContent = `${pending.tags.length.toLocaleString()} tags ready. Save Settings to apply.`;
|
|
} catch (error) {
|
|
if (dialog.scope.signal.aborted) return;
|
|
status.dataset.error = 'true';
|
|
status.textContent = `Could not load tags: ${error.message}`;
|
|
} finally {
|
|
busy = false;
|
|
load.disabled = file.disabled = save.disabled = false;
|
|
}
|
|
};
|
|
dialog.scope.listen(load, 'click', () => {
|
|
if (!url.value.trim() || !url.reportValidity()) {
|
|
status.dataset.error = 'true';
|
|
status.textContent = 'Enter a valid tag-list URL.';
|
|
return;
|
|
}
|
|
prepare(() => autocomplete.fetchTagList(url.value.trim(), dialog.scope.signal));
|
|
});
|
|
dialog.scope.listen(file, 'change', () => {
|
|
const selected = file.files[0];
|
|
if (!selected) return;
|
|
prepare(async () => {
|
|
if (selected.size > MAX_TAG_BYTES) throw new Error('Tag file too large. Maximum size is 32 MB.');
|
|
const text = await selected.text();
|
|
const tags = await parseTagFile(text, dialog.scope.signal);
|
|
return { text, tags, source: `file:${selected.name}` };
|
|
});
|
|
file.value = '';
|
|
});
|
|
dialog.scope.listen(cancel, 'click', () => dialog.close('cancel'));
|
|
dialog.scope.listen(form, 'submit', async event => {
|
|
event.preventDefault();
|
|
if (busy) return;
|
|
const next = {};
|
|
for (const [key, input] of fields) next[key] = input.type === 'checkbox' ? input.checked : input.type === 'number' ? Number(input.value) : input.value.trim();
|
|
next.embeddingTrigger ||= 'embedding:';
|
|
next.loraTrigger ||= '<lora:';
|
|
next.tagSource = pending?.source || (next.customWordsUrl !== autocomplete.settings.customWordsUrl ? next.customWordsUrl : autocomplete.settings.tagSource);
|
|
save.disabled = true;
|
|
try {
|
|
await autocomplete.saveTagSettings(next, pending);
|
|
dialog.close('save');
|
|
} catch (error) {
|
|
status.dataset.error = 'true';
|
|
status.textContent = error.message;
|
|
save.disabled = false;
|
|
}
|
|
});
|
|
dialog.scope.cleanup(() => { if (autocomplete.settingsDialog === dialog) autocomplete.settingsDialog = null; });
|
|
dialog.open(fields.get('enabled'));
|
|
return dialog;
|
|
}
|
|
|
|
export function createAutocompleteLauncher(autocomplete, settings, fallbackFocus) {
|
|
if (!customElements.get('kiko-autocomplete-launcher')) {
|
|
customElements.define('kiko-autocomplete-launcher', class extends HTMLElement {
|
|
connectedCallback() {
|
|
if (this.scope && !this.scope.signal.aborted) return;
|
|
this.scope = createScope();
|
|
this.className = 'ca-tools ca-autocomplete-launcher';
|
|
mountTheme(this, this.settings, this.scope);
|
|
const button = createButton('Open Settings', 'ca-primary');
|
|
this.replaceChildren(button);
|
|
this.scope.listen(button, 'click', () => {
|
|
this.dialog = openAutocompleteSettings(this.autocomplete, this.settings, this.fallbackFocus);
|
|
this.dialog.owner = this;
|
|
});
|
|
this.scope.cleanup(() => { if (this.dialog?.owner === this) this.dialog.close('removed'); });
|
|
}
|
|
disconnectedCallback() {
|
|
const scope = this.scope;
|
|
queueMicrotask(() => { if (!this.isConnected) scope.dispose(); });
|
|
}
|
|
});
|
|
}
|
|
const row = document.createElement('tr');
|
|
row.className = 'kikotools-settings-row';
|
|
const label = document.createElement('td');
|
|
label.textContent = 'Embedding Autocomplete Configuration';
|
|
const value = document.createElement('td');
|
|
const launcher = document.createElement('kiko-autocomplete-launcher');
|
|
Object.assign(launcher, { autocomplete, settings, fallbackFocus });
|
|
value.append(launcher);
|
|
row.append(label, value);
|
|
return row;
|
|
}
|