Files
DemonGatanjieu-Anomalous_Mo…/web/modules/quick_search_data.js
T
DemonGatanjieuandClaude Opus 5.5 58b11deae0 Quick search on the canvas (Alt+B)
A bar over the canvas finds combos, models and saved prompts as you type
(GET /anomalous/quick_index lists the models in a few words) and puts the chosen one
where the pointer was. Also in the empty canvas's right-click menu, now registered.
Saved model paths in a linked model folder get their real place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:34:41 +08:00

119 lines
4.5 KiB
JavaScript

/**
* What the canvas's quick search (ui_quick_search.js) looks through, and how it matches: the
* models a loader node can take (GET /anomalous/quick_index), the combos and the saved
* prompts, each read when the bar opens and kept a minute. `searchQuick` is the matching
* itself: every word typed must appear in an item's name, file, base model, trigger words or
* text; names that start with the words come first. No DOM.
*/
// The loader node each model type goes on the canvas with (ui_grid.js applyModelToCanvas).
export const MODEL_LOADERS = Object.freeze({
checkpoints: 'CheckpointLoaderSimple',
loras: 'LoraLoader',
unet: 'UNETLoader',
diffusion_models: 'UNETLoader',
vae: 'VAELoader',
clip: 'CLIPLoader',
controlnet: 'ControlNetLoader',
});
export const KINDS = Object.freeze(['combo', 'model', 'prompt']);
const KEEP_MS = 60 * 1000;
const PER_KIND = 8;
const stem = name => String(name || '').split(/[\\/]/).pop().replace(/\.[^.]+$/, '');
const lower = value => String(value || '').toLowerCase();
/** The words of `query`, lower case. */
export const queryTerms = query => lower(query).split(/\s+/).filter(Boolean);
/** A model (GET /anomalous/quick_index) as a search item. */
export function modelItem(model) {
const title = model.name || model.civitai_name || stem(model.filename);
return {
kind: 'model',
key: `model|${model.type}|${model.subfolder}|${model.filename}`,
title,
type: model.type,
detail: [model.base, model.subfolder].filter(Boolean).join(' · '),
thumb: model.preview_url || '',
haystack: lower([title, model.civitai_name, model.filename, model.subfolder, model.base, ...(model.words || [])].join(' ')),
data: model,
};
}
/** A combo (GET /anomalous/notebooks) as a search item; `detail` and `thumb` come from the caller. */
export function comboItem(note, { detail = '', thumb = '', text = '' } = {}) {
return {
kind: 'combo',
key: `combo|${note.filename}`,
title: note.name,
detail,
thumb,
haystack: lower([note.name, detail, text].join(' ')),
data: note,
};
}
/** A saved prompt card ({ id, title, content, role }) as a search item. */
export function promptItem(card) {
return {
kind: 'prompt',
key: `prompt|${card.id}`,
title: card.title,
detail: card.content.replace(/\s+/g, ' ').slice(0, 90),
role: card.role,
haystack: lower([card.title, card.content].join(' ')),
data: card,
};
}
/** 0: the title starts with the words, 1: the title holds them, 2: elsewhere. */
function rank(item, terms) {
const title = lower(item.title);
if (title.startsWith(terms.join(' '))) return 0;
return terms.every(term => title.includes(term)) ? 1 : 2;
}
/**
* The items matching `query` among `items`, grouped by kind in KINDS' order (only `kind` when
* given), at most `perKind` of each. With no words: the combos only, as they are.
*/
export function searchQuick(items, query, { kind = '', perKind = PER_KIND } = {}) {
const terms = queryTerms(query);
const wanted = kind ? [kind] : KINDS;
const groups = [];
for (const each of wanted) {
let found = items.filter(item => item.kind === each);
if (terms.length) {
found = found.filter(item => terms.every(term => item.haystack.includes(term)))
.map((item, index) => ({ item, index, rank: rank(item, terms) }))
.sort((a, b) => a.rank - b.rank || a.index - b.index)
.map(entry => entry.item);
} else if (!kind && each !== 'combo') {
found = [];
}
if (found.length) groups.push({ kind: each, items: found.slice(0, perKind), more: Math.max(0, found.length - perKind) });
}
return groups;
}
let cache = null; // { at, models, combos, prompts }
/** The kept lists when younger than a minute, else null. */
export function cachedSources(now = Date.now()) {
return cache && now - cache.at < KEEP_MS ? cache : null;
}
export function keepSources(sources, now = Date.now()) {
cache = { ...sources, at: now };
return cache;
}
/** The models a loader node can take, as search items. */
export async function loadModelItems(signal) {
const response = await fetch('/anomalous/quick_index', { signal });
if (!response.ok) throw new Error(`quick index ${response.status}`);
const { models = [] } = await response.json();
return models.filter(model => MODEL_LOADERS[model.type]).map(modelItem);
}