refactor: unify loaded model grouping
This commit is contained in:
@@ -37,6 +37,10 @@ import { getSha256Field, normalizeSha256 } from '../web/resolver/utils/hash_util
|
||||
import { normalizeSearchToken } from '../web/resolver/utils/search_utils.js';
|
||||
import { parseFiniteNumber } from '../web/resolver/utils/size_utils.js';
|
||||
import { getSourceDisplayLabel, normalizeSourceKey } from '../web/resolver/utils/source_labels.js';
|
||||
import {
|
||||
buildLoadedModelTokenStrings,
|
||||
groupLoadedModelsByCategory,
|
||||
} from '../web/resolver/utils/loaded_model_utils.js';
|
||||
import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js';
|
||||
import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js';
|
||||
import { missingModelStateMethods } from '../web/resolver/views/missing_model_state_methods.js';
|
||||
@@ -57,8 +61,11 @@ void CATEGORY_ALIASES;
|
||||
void normalizeCategoryToken;
|
||||
void normalizeSha256;
|
||||
void pollBackgroundTask;
|
||||
void buildLoadedModelTokenStrings;
|
||||
void groupLoadedModelsByCategory;
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, '..');
|
||||
const getSvgIcon = () => '';
|
||||
const queueMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/actions/queue_methods.js'),
|
||||
'utf8'
|
||||
@@ -2068,6 +2075,66 @@ test('Loaded Models progress patches the stable progress container', () => {
|
||||
assert.doesNotMatch(pollAnalysisProgress, /contentElement\.innerHTML\s*=/);
|
||||
});
|
||||
|
||||
test('loaded model grouping and copy strings stay consistent across render paths', () => {
|
||||
const updateLoadedModelCopyValues = eval(
|
||||
`(${extractMethod(tabsLoadedMethodsSource, 'updateLoadedModelCopyValues')})`
|
||||
);
|
||||
const displayLoadedModels = eval(
|
||||
`(${extractMethod(tabsLoadedMethodsSource, 'displayLoadedModels')})`
|
||||
);
|
||||
const models = [
|
||||
{ category: 'loras', name: 'active-lora', active: true, connected: true },
|
||||
{ category: 'loras', name: 'inactive-lora', active: false, connected: true },
|
||||
{ category: 'checkpoints', name: 'default-checkpoint' },
|
||||
{ name: 'unknown-inactive', connected: false },
|
||||
];
|
||||
const getModelToken = (model, category) => `<${category}:${model.name}>`;
|
||||
const dialog = {
|
||||
getModelToken,
|
||||
escapeJsString: value => JSON.stringify(value),
|
||||
getCategoryDisplayName: category => category,
|
||||
escapeHtml: value => String(value),
|
||||
getModelNameAndStrength: model => ({ name: model.name, strength: null }),
|
||||
getLoadedModelDomKey: model => model.name,
|
||||
getLoadedModelContext: model => model,
|
||||
getContextMenuAttrs: () => '',
|
||||
getModelPreviewTooltipAttrs: () => '',
|
||||
};
|
||||
const expectedUpdate = {
|
||||
active: '<loras:active-lora> <checkpoints:default-checkpoint>',
|
||||
inactive: '<loras:inactive-lora> <unknown:unknown-inactive>',
|
||||
all: '<loras:active-lora> <loras:inactive-lora> '
|
||||
+ '<checkpoints:default-checkpoint> <unknown:unknown-inactive>',
|
||||
};
|
||||
const expectedDisplay = {
|
||||
active: '<loras:active-lora> <checkpoints:default-checkpoint> ',
|
||||
inactive: '<loras:inactive-lora> <unknown:unknown-inactive>',
|
||||
all: expectedUpdate.all,
|
||||
};
|
||||
|
||||
const updateContainer = { dataset: {} };
|
||||
updateLoadedModelCopyValues.call(dialog, updateContainer, models);
|
||||
|
||||
const displayContainer = { dataset: {}, innerHTML: '' };
|
||||
displayLoadedModels.call(dialog, displayContainer, {
|
||||
loaded_models: models,
|
||||
total: models.length,
|
||||
});
|
||||
|
||||
assert.deepEqual({
|
||||
active: updateContainer.dataset.mlActiveString,
|
||||
inactive: updateContainer.dataset.mlInactiveString,
|
||||
all: updateContainer.dataset.mlAllString,
|
||||
}, expectedUpdate);
|
||||
assert.deepEqual({
|
||||
active: displayContainer.dataset.mlActiveString,
|
||||
inactive: displayContainer.dataset.mlInactiveString,
|
||||
all: displayContainer.dataset.mlAllString,
|
||||
}, expectedDisplay);
|
||||
assert.notEqual(displayContainer.innerHTML, '');
|
||||
void getSvgIcon;
|
||||
});
|
||||
|
||||
test('workflow progress polling preserves endpoint, rendering, and token cleanup', async () => {
|
||||
const pollWorkflowProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'pollWorkflowProgress')})`);
|
||||
const pollAnalysisProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'pollAnalysisProgress')})`);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
export function groupLoadedModelsByCategory(loadedModels = []) {
|
||||
const byCategory = {};
|
||||
|
||||
for (const model of loadedModels) {
|
||||
const category = model.category || 'unknown';
|
||||
if (!byCategory[category]) {
|
||||
byCategory[category] = { active: [], inactive: [] };
|
||||
}
|
||||
|
||||
const isActive = model.active !== false && model.connected !== false;
|
||||
byCategory[category][isActive ? 'active' : 'inactive'].push(model);
|
||||
}
|
||||
|
||||
return byCategory;
|
||||
}
|
||||
|
||||
export function buildLoadedModelTokenStrings(
|
||||
byCategory,
|
||||
getToken,
|
||||
{ preserveEmptyCategories = false } = {}
|
||||
) {
|
||||
const build = filter => {
|
||||
const categoryStrings = Object.entries(byCategory).map(([category, models]) => {
|
||||
const selected = filter === 'all'
|
||||
? [...models.active, ...models.inactive]
|
||||
: models[filter];
|
||||
return selected.map(model => getToken(model, category)).join(' ');
|
||||
});
|
||||
|
||||
return (preserveEmptyCategories
|
||||
? categoryStrings
|
||||
: categoryStrings.filter(Boolean)
|
||||
).join(' ');
|
||||
};
|
||||
|
||||
return {
|
||||
active: build('active'),
|
||||
inactive: build('inactive'),
|
||||
all: build('all'),
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
getCustomNodeOriginalIdentity,
|
||||
} from "../custom_nodes/registry.js";
|
||||
import { normalizePathIdentity } from "../utils/html_utils.js";
|
||||
import {
|
||||
buildLoadedModelTokenStrings,
|
||||
groupLoadedModelsByCategory,
|
||||
} from "../utils/loaded_model_utils.js";
|
||||
export const tabsLoadedMethods = {
|
||||
getTabButton(tab) {
|
||||
return {
|
||||
@@ -207,31 +211,11 @@ export const tabsLoadedMethods = {
|
||||
updateLoadedModelCopyValues(container, loadedModels = []) {
|
||||
if (!container) return;
|
||||
|
||||
const byCategory = {};
|
||||
for (const model of loadedModels) {
|
||||
const category = model.category || 'unknown';
|
||||
if (!byCategory[category]) {
|
||||
byCategory[category] = { active: [], inactive: [] };
|
||||
}
|
||||
const filter = model.active !== false && model.connected !== false
|
||||
? 'active'
|
||||
: 'inactive';
|
||||
byCategory[category][filter].push(model);
|
||||
}
|
||||
|
||||
const buildString = (filter) => Object.entries(byCategory)
|
||||
.flatMap(([category, models]) => {
|
||||
const selected = filter === 'all'
|
||||
? [...models.active, ...models.inactive]
|
||||
: models[filter];
|
||||
return selected.map(model => this.getModelToken(model, category));
|
||||
})
|
||||
.join(' ');
|
||||
const strings = {
|
||||
active: buildString('active'),
|
||||
inactive: buildString('inactive'),
|
||||
all: buildString('all'),
|
||||
};
|
||||
const byCategory = groupLoadedModelsByCategory(loadedModels);
|
||||
const strings = buildLoadedModelTokenStrings(
|
||||
byCategory,
|
||||
(model, category) => this.getModelToken(model, category),
|
||||
);
|
||||
|
||||
container.dataset.mlActiveString = strings.active;
|
||||
container.dataset.mlInactiveString = strings.inactive;
|
||||
@@ -353,40 +337,20 @@ export const tabsLoadedMethods = {
|
||||
return;
|
||||
}
|
||||
|
||||
const byCategory = {};
|
||||
|
||||
for (const model of loadedModels) {
|
||||
const cat = model.category || 'unknown';
|
||||
if (!byCategory[cat]) {
|
||||
byCategory[cat] = { active: [], inactive: [] };
|
||||
}
|
||||
|
||||
// Some loaders expose per-model enablement separately from links.
|
||||
const isActive = model.active !== false && model.connected !== false;
|
||||
|
||||
if (isActive) {
|
||||
byCategory[cat].active.push(model);
|
||||
} else {
|
||||
byCategory[cat].inactive.push(model);
|
||||
}
|
||||
}
|
||||
const byCategory = groupLoadedModelsByCategory(loadedModels);
|
||||
|
||||
const activeCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.active.length, 0);
|
||||
const inactiveCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.inactive.length, 0);
|
||||
|
||||
const buildCategoryStrings = (filter) => {
|
||||
const result = {};
|
||||
for (const [category, modelsObj] of Object.entries(byCategory)) {
|
||||
const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive];
|
||||
const parts = models.map(model => this.getModelToken(model, category));
|
||||
result[category] = parts.join(' ');
|
||||
}
|
||||
return Object.values(result).join(' ');
|
||||
};
|
||||
|
||||
const activeString = buildCategoryStrings('active');
|
||||
const inactiveString = buildCategoryStrings('inactive');
|
||||
const allString = buildCategoryStrings('all');
|
||||
const {
|
||||
active: activeString,
|
||||
inactive: inactiveString,
|
||||
all: allString,
|
||||
} = buildLoadedModelTokenStrings(
|
||||
byCategory,
|
||||
(model, category) => this.getModelToken(model, category),
|
||||
{ preserveEmptyCategories: true },
|
||||
);
|
||||
|
||||
const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon');
|
||||
const copyButtonHtml = (text, extraClass = '') => `
|
||||
|
||||
Reference in New Issue
Block a user