Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_model_guide.js
T
DemonGatanjieuandClaude Opus 5.5 c51fd11e03 Show a model's guessed words, its author's settings and the files it needs in its details
- ui_model_guide.js (above the description, out of ui_detail.js): Civitai's trigger words,
  else the words the LoRA's file suggests (dashed, said to be a guess), the settings its
  author's example pictures used, and for a diffusion models file of a known family the text
  encoder and VAE it needs with the file of that name found here; one GET
  /anomalous/model_guide per page, a newer page cancels it.
- The models page's search finds the header words too.
- Checked on the isolated test ComfyUI with stand-in files: words read now and after an
  offline scan, three example pictures summed up, Anima's two parts found by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:52:33 +08:00

139 lines
5.7 KiB
JavaScript

/**
* ui_model_guide.js
* The model details' words and settings above the description: Civitai's trigger words, else the
* words the LoRA's own file suggests (a guess, marked as one), the settings its author's example
* pictures used, and for a model loaded in parts the text encoder and VAE it needs with the file of
* that name found here. All but Civitai's words come from GET /anomalous/model_guide
* (api/model_guide.py), asked once per details page; a newer page cancels the request, and a page
* left before it answers is not filled.
*/
import { translate } from './locales.js';
const t = (key, params) => translate(key, params);
let asking = null;
function copyButton(words) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'anomalous-guide-copy';
button.textContent = t('copyAll');
button.onclick = () => navigator.clipboard.writeText(words.join(', ')).then(() => {
button.textContent = t('copied');
setTimeout(() => { button.textContent = t('copyAll'); }, 1500);
});
return button;
}
function section(title, ...extra) {
const box = document.createElement('div');
box.className = 'anomalous-guide-section';
const head = document.createElement('div');
head.className = 'anomalous-guide-head';
const label = document.createElement('strong');
label.textContent = title;
head.append(label, ...extra);
box.appendChild(head);
return box;
}
function wordsSection(title, words, hint = '') {
const box = section(title, copyButton(words));
if (hint) {
const line = document.createElement('div');
line.className = 'anomalous-guide-hint';
line.textContent = hint;
box.appendChild(line);
}
const list = document.createElement('div');
list.className = 'anomalous-guide-words';
for (const word of words) {
const chip = document.createElement('span');
chip.className = hint ? 'anomalous-guide-word is-guess' : 'anomalous-guide-word';
chip.textContent = word;
chip.title = t('clickToCopy') + word;
chip.onclick = () => navigator.clipboard.writeText(word).then(() => {
chip.classList.add('is-copied');
setTimeout(() => chip.classList.remove('is-copied'), 500);
});
list.appendChild(chip);
}
box.appendChild(list);
return box;
}
/** "Euler a", or "Euler a (2/3)" when not every picture used it. */
function most(value, pictures) {
return value[1] < pictures ? `${value[0]} (${value[1]}/${pictures})` : String(value[0]);
}
function settingsSection(examples) {
const { pictures } = examples;
const items = [
examples.sampler && most(examples.sampler, pictures),
examples.scheduler && most(examples.scheduler, pictures),
examples.steps && t('guideSteps', { steps: examples.steps }),
examples.cfg && `CFG ${examples.cfg}`,
examples.size && most(examples.size, pictures),
examples.clip_skip && `Clip skip ${examples.clip_skip}`,
examples.lora_weight && t('guideLoraWeight', { weight: examples.lora_weight }),
].filter(Boolean);
const box = section(t('guideExamples', { count: pictures }));
const list = document.createElement('div');
list.className = 'anomalous-guide-settings';
for (const item of items) {
const pill = document.createElement('span');
pill.className = 'anomalous-guide-setting';
pill.textContent = item;
list.appendChild(pill);
}
box.appendChild(list);
return box;
}
function partsSection(parts) {
const box = section(t('guideParts'));
for (const part of parts) {
const row = document.createElement('div');
row.className = part.found ? 'anomalous-guide-part' : 'anomalous-guide-part is-missing';
const role = document.createElement('span');
role.className = 'anomalous-guide-part-role';
role.textContent = t(`guidePart_${part.folder}`);
const name = document.createElement('code');
name.textContent = part.file;
const state = document.createElement('span');
state.className = 'anomalous-guide-part-state';
state.textContent = part.found ? `✓ ${t('guidePartFound', { file: part.found })}` : t('guidePartMissing');
row.append(role, name, state);
box.appendChild(row);
}
return box;
}
/** The section for `model` (a listed model: type, path_idx, subfolder, filename, metadata). */
export function createModelGuide(model) {
const box = document.createElement('div');
box.className = 'anomalous-guide';
const trained = (model.metadata?.trainedWords || []).filter(word => String(word).trim());
if (trained.length) box.appendChild(wordsSection(t('detailTrainedWords'), trained));
asking?.abort();
asking = null;
if (!model.type || !model.filename) return box;
const request = new AbortController();
asking = request;
const query = new URLSearchParams({
type: model.type, path_idx: String(model.path_idx ?? 0), subfolder: model.subfolder || '', filename: model.filename,
});
fetch(`/anomalous/model_guide?${query}`, { signal: request.signal })
.then(response => (response.ok ? response.json() : null))
.then(guide => {
if (!guide || request.signal.aborted || !box.isConnected) return;
if (!trained.length && guide.words?.length) box.appendChild(wordsSection(t('guideHeaderWords'), guide.words, t('guideHeaderWordsHelp')));
if (guide.examples?.pictures) box.appendChild(settingsSection(guide.examples));
if (guide.parts?.length) box.appendChild(partsSection(guide.parts));
})
.catch(error => {
if (error.name !== 'AbortError') console.warn('[Anomalous] model guide:', error);
});
return box;
}