import { log } from './logger'; /* * Extra-networks completion for SD.Next prompt textareas. * * Companion to autocomplete.js: exposes sorted indices for LoRAs, embeddings, and wildcards, * each backed by an existing enumeration endpoint. Dispatch and insertion are driven from * autocomplete.js via the mode returned by getCurrentWord(). */ interface XnItem { name: string; display?: string; } interface SearchItem { name: string; display: string; } type SearchResult = SearchItem & { kind: 'lora' | 'embed' | 'wildcard' }; /** Binary search for the first item where item.name >= query. */ export function lowerBound(items: { name: string }[], query: string): number { let lo = 0; let hi = items.length; while (lo < hi) { const mid = (lo + hi) >>> 1; if (items[mid].name < query) lo = mid + 1; else hi = mid; } return lo; } interface XnEngine { lora: XnIndex; embed: XnIndex; wildcard: XnIndex; fetchJson(path: string): Promise; loadAll(): Promise; searchLoras(prefix: string, limit?: number): SearchResult[]; searchEmbeddings(prefix: string, limit?: number): SearchResult[]; searchWildcards(prefix: string, limit?: number): SearchResult[]; } // -- Indices -- class XnIndex { items: SearchItem[]; constructor(items: XnItem[]) { // items: [{ name, display }]. Sorted in-place by lowercase name. this.items = items.map(({ name, display }) => ({ name: String(name).toLowerCase(), display: display ?? name, })); this.items.sort((a, b) => a.name.localeCompare(b.name)); } search(prefix: string, limit = 20): SearchItem[] { const query = String(prefix).toLowerCase(); // Empty query returns the first `limit` items so `= 3) { for (let i = 0; i < this.items.length && matches.length < limit; i++) { if (this.items[i].name.includes(query)) matches.push(this.items[i]); } } return matches.slice(0, limit); } } // -- Engine -- export const xnEngine: XnEngine = { lora: new XnIndex([]), embed: new XnIndex([]), wildcard: new XnIndex([]), async fetchJson(path) { try { // const resp = await fetch(`${window.api}${path}`, { credentials: 'include' }); const resp = await authFetch(`${window.api}${path}`); if (!resp?.ok) throw new Error(`${resp?.status}`); return await resp.json(); } catch (e) { log('autoComplete', { xnFetchFailed: path, error: String(e) }); return null; } }, async loadAll() { // LoRAs: [{name, alias, path, metadata}, ...] const loraData = await this.fetchJson('/loras'); if (Array.isArray(loraData)) { const items: XnItem[] = []; for (const lo of loraData) { if (typeof lo === 'object' && lo && 'name' in lo && typeof lo.name === 'string') items.push({ name: lo.name }); if (typeof lo === 'object' && lo && 'alias' in lo && typeof lo.alias === 'string' && lo.alias !== (lo as { name?: string }).name) items.push({ name: lo.alias }); } this.lora = new XnIndex(items); } // Embeddings: {loaded: [...], skipped: [...]} if (window.opts.diffusers_enable_embed) { const embData = await this.fetchJson('/embeddings') as Record | null; if (embData && typeof embData === 'object') { const loaded = Array.isArray(embData.loaded) ? embData.loaded : []; this.embed = new XnIndex(loaded.map((name) => ({ name: String(name) }))); } } else { this.embed = new XnIndex([]); } // Wildcards: [{name}, ...] if (window.opts.wildcards_enabled) { const wcData = await this.fetchJson('/wildcards'); if (Array.isArray(wcData)) { this.wildcard = new XnIndex( wcData .filter((w) => typeof w === 'object' && w && 'name' in w && typeof w.name === 'string') .map((w) => ({ name: w.name })), ); } } else { this.wildcard = new XnIndex([]); } log('autoComplete', { xnLoaded: true, lora: this.lora.items.length, embed: this.embed.items.length, wildcard: this.wildcard.items.length, }); }, searchLoras(prefix, limit = 20) { return this.lora.search(prefix, limit).map((item) => ({ ...item, kind: 'lora' as const })); }, searchEmbeddings(prefix, limit = 20) { return this.embed.search(prefix, limit).map((item) => ({ ...item, kind: 'embed' as const })); }, searchWildcards(prefix, limit = 20) { return this.wildcard.search(prefix, limit).map((item) => ({ ...item, kind: 'wildcard' as const })); }, };