Files
Azornes-Comfyui-Model-Resolver/web/resolver/node_context_menu.js
T

219 lines
6.9 KiB
JavaScript

function normalizeIdentity(value) {
return String(value ?? '').trim().replaceAll('\\', '/').toLowerCase();
}
function getResolvedModelPath(model = {}) {
return String(model.full_path || model.path || model.resolved_path || '').trim();
}
function getResolvedModelFilename(model = {}) {
const source = String(
model.name
|| model.filename
|| model.original_path
|| model.relative_path
|| getResolvedModelPath(model)
|| 'Resolved model'
);
return source.split(/[\\/]/).pop() || source;
}
function getResolvedModelCategoryLabel(model = {}, formatCategory = null) {
const category = String(model.category || 'unknown').trim() || 'unknown';
const formatted = typeof formatCategory === 'function'
? formatCategory(category)
: category.replaceAll('_', ' ');
return String(formatted || category).toUpperCase();
}
function getResolvedModelMenuLabel(model = {}, formatCategory = null) {
const category = getResolvedModelCategoryLabel(model, formatCategory);
const widgetName = String(model.widget_name || '').trim();
const filename = getResolvedModelFilename(model);
const slot = widgetName ? `${category} · ${widgetName}` : category;
return `${slot} — ${filename}`;
}
function getWidgetChoiceValue(choice) {
if (choice && typeof choice === 'object') {
return choice.value ?? choice.name ?? choice.label ?? '';
}
return choice;
}
function isCurrentWidgetSelection(widget, value) {
const normalizedValue = normalizeIdentity(value);
if (!normalizedValue || ['none', 'null', 'undefined'].includes(normalizedValue)) {
return false;
}
const choices = widget?.options?.values;
if (!Array.isArray(choices) || !choices.length) {
return true;
}
return choices.some(choice => (
normalizeIdentity(getWidgetChoiceValue(choice)) === normalizedValue
));
}
function createModelActions(model, handlers = {}) {
return [
{
content: 'Show in Model Resolver',
callback: () => handlers.showInResolver?.(model),
},
{
content: 'Show info',
callback: () => handlers.showInfo?.(model),
},
{
content: 'Open containing folder',
callback: () => handlers.openContainingFolder?.(model),
},
];
}
export function isExistingResolvedModel(model = {}) {
return model.exists === true && Boolean(getResolvedModelPath(model));
}
export function matchesWorkflowModelReference(model = {}, reference = {}) {
if (String(model.node_id ?? '') !== String(reference.node_id ?? '')) {
return false;
}
if (
reference.widget_index !== undefined
&& reference.widget_index !== null
&& String(model.widget_index ?? '') !== String(reference.widget_index)
) {
return false;
}
if (
reference.is_top_level !== undefined
&& reference.is_top_level !== null
&& Boolean(model.is_top_level) !== Boolean(reference.is_top_level)
) {
return false;
}
if (
reference.subgraph_id
&& String(model.subgraph_id || '') !== String(reference.subgraph_id)
) {
return false;
}
if (reference.original_path) {
return normalizeIdentity(model.original_path) === normalizeIdentity(reference.original_path);
}
return true;
}
export function getResolvedModelsForNode(data = {}, nodeId, scope = {}) {
const nodeReference = {
node_id: nodeId,
is_top_level: scope.is_top_level,
subgraph_id: scope.subgraph_id,
};
return (data.resolved_models || [])
.filter(isExistingResolvedModel)
.filter(model => matchesWorkflowModelReference(model, nodeReference))
.sort((left, right) => {
const widgetOrder = Number(left.widget_index ?? 0) - Number(right.widget_index ?? 0);
if (widgetOrder) return widgetOrder;
return getResolvedModelMenuLabel(left).localeCompare(getResolvedModelMenuLabel(right));
});
}
export function getImmediateModelsForNode(data = {}, node = {}, scope = {}) {
const references = [
...(data.resolved_models || []),
...(data.missing_models || []),
].filter(model => (
(!model.node_type || !node?.type || model.node_type === node.type)
&& matchesWorkflowModelReference(model, {
node_id: node?.id,
is_top_level: scope.is_top_level,
subgraph_id: scope.subgraph_id,
})
));
const modelsByWidget = new Map();
for (const reference of references) {
const widgetIndex = Number(reference.widget_index);
if (!Number.isInteger(widgetIndex) || modelsByWidget.has(widgetIndex)) continue;
const widget = node?.widgets?.[widgetIndex];
const currentValue = widget?.value;
if (typeof currentValue !== 'string') continue;
if (!isCurrentWidgetSelection(widget, currentValue)) continue;
if (normalizeIdentity(currentValue) === normalizeIdentity(reference.original_path)) {
if (isExistingResolvedModel(reference)) {
modelsByWidget.set(widgetIndex, reference);
}
continue;
}
modelsByWidget.set(widgetIndex, {
...reference,
name: String(currentValue),
filename: getResolvedModelFilename({ original_path: currentValue }),
original_path: String(currentValue),
full_path: '',
path: '',
resolved_path: '',
exists: false,
resolution_pending: true,
});
}
return [...modelsByWidget.values()].sort((left, right) => (
Number(left.widget_index ?? 0) - Number(right.widget_index ?? 0)
));
}
export function buildModelResolverNodeMenu(models = [], handlers = {}, options = {}) {
const resolvedModels = models.filter(model => (
isExistingResolvedModel(model) || model.resolution_pending === true
));
if (!resolvedModels.length) return null;
const submenuOptions = resolvedModels.length === 1
? createModelActions(resolvedModels[0], handlers)
: resolvedModels.map(model => ({
title: getResolvedModelMenuLabel(model, options.formatCategory),
has_submenu: true,
submenu: {
options: createModelActions(model, handlers),
},
}));
return {
title: 'Model Resolver',
className: 'mdi mdi-link-variant mr-model-resolver-node-menu',
has_submenu: true,
submenu: {
options: submenuOptions,
},
};
}
export function toResolverContextModel(model = {}) {
const path = getResolvedModelPath(model);
const filename = getResolvedModelFilename(model);
return {
...model,
name: model.name || filename,
filename,
path,
resolved_path: path,
relative_path: model.relative_path || model.original_path || filename,
context_scope: 'local_model',
};
}