frontend: add Model Resolver node context menu action and workflow selection support
This commit is contained in:
@@ -4,6 +4,12 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { html, normalizePathIdentity } from '../web/resolver/utils/html_utils.js';
|
||||
import { getModelCardUrl } from '../web/resolver/utils/url_utils.js';
|
||||
import {
|
||||
buildModelResolverNodeMenu,
|
||||
getResolvedModelsForNode,
|
||||
matchesWorkflowModelReference,
|
||||
toResolverContextModel,
|
||||
} from '../web/resolver/node_context_menu.js';
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, '..');
|
||||
const queueMethodsSource = fs.readFileSync(
|
||||
@@ -43,6 +49,194 @@ const renderFormatMethodsSource = fs.readFileSync(
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('node context menu includes only existing resolved models from the selected node scope', () => {
|
||||
const data = {
|
||||
resolved_models: [
|
||||
{
|
||||
node_id: 42,
|
||||
widget_index: 0,
|
||||
widget_name: 'ckpt_name',
|
||||
original_path: 'models/checkpoint.safetensors',
|
||||
full_path: 'E:\\models\\checkpoint.safetensors',
|
||||
category: 'checkpoints',
|
||||
exists: true,
|
||||
is_top_level: true,
|
||||
},
|
||||
{
|
||||
node_id: 42,
|
||||
widget_index: 1,
|
||||
widget_name: 'vae_name',
|
||||
original_path: 'vae.safetensors',
|
||||
full_path: 'E:\\models\\vae\\vae.safetensors',
|
||||
category: 'vae',
|
||||
exists: false,
|
||||
is_top_level: true,
|
||||
},
|
||||
{
|
||||
node_id: 42,
|
||||
widget_index: 0,
|
||||
original_path: 'nested.safetensors',
|
||||
full_path: 'E:\\models\\nested.safetensors',
|
||||
category: 'checkpoints',
|
||||
exists: true,
|
||||
is_top_level: false,
|
||||
subgraph_id: 'subgraph-a',
|
||||
},
|
||||
{
|
||||
node_id: 99,
|
||||
widget_index: 0,
|
||||
original_path: 'other.safetensors',
|
||||
full_path: 'E:\\models\\other.safetensors',
|
||||
category: 'checkpoints',
|
||||
exists: true,
|
||||
is_top_level: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const models = getResolvedModelsForNode(data, 42, { is_top_level: true });
|
||||
|
||||
assert.equal(models.length, 1);
|
||||
assert.equal(models[0].widget_name, 'ckpt_name');
|
||||
});
|
||||
|
||||
test('node context menu groups multiple model inputs and dispatches actions for the selected model', () => {
|
||||
const calls = [];
|
||||
const models = [
|
||||
{
|
||||
node_id: 7,
|
||||
widget_index: 0,
|
||||
widget_name: 'ckpt_name',
|
||||
original_path: 'model.safetensors',
|
||||
full_path: 'E:\\models\\model.safetensors',
|
||||
category: 'checkpoints',
|
||||
exists: true,
|
||||
},
|
||||
{
|
||||
node_id: 7,
|
||||
widget_index: 1,
|
||||
widget_name: 'vae_name',
|
||||
original_path: 'vae.safetensors',
|
||||
full_path: 'E:\\models\\vae\\vae.safetensors',
|
||||
category: 'vae',
|
||||
exists: true,
|
||||
},
|
||||
];
|
||||
const menu = buildModelResolverNodeMenu(models, {
|
||||
showInResolver: model => calls.push(['show', model.widget_name]),
|
||||
showInfo: model => calls.push(['info', model.widget_name]),
|
||||
openContainingFolder: model => calls.push(['folder', model.widget_name]),
|
||||
}, {
|
||||
formatCategory: category => category === 'checkpoints' ? 'Checkpoint' : category,
|
||||
});
|
||||
|
||||
assert.equal(menu.title, 'Model Resolver');
|
||||
assert.equal(menu.submenu.options.length, 2);
|
||||
assert.match(menu.submenu.options[0].title, /^CHECKPOINT · ckpt_name/);
|
||||
assert.match(menu.submenu.options[1].title, /^VAE · vae_name/);
|
||||
assert.deepEqual(
|
||||
menu.submenu.options[0].submenu.options.map(option => option.content),
|
||||
['Show in Model Resolver', 'Show info', 'Open containing folder']
|
||||
);
|
||||
|
||||
menu.submenu.options[1].submenu.options[2].callback();
|
||||
assert.deepEqual(calls, [['folder', 'vae_name']]);
|
||||
});
|
||||
|
||||
test('node context menu uses direct actions for one model and converts its local path for resolver actions', () => {
|
||||
const model = {
|
||||
node_id: 5,
|
||||
widget_index: 2,
|
||||
original_path: 'folder/model.gguf',
|
||||
full_path: 'E:\\models\\folder\\model.gguf',
|
||||
category: 'diffusion_models',
|
||||
exists: true,
|
||||
};
|
||||
const menu = buildModelResolverNodeMenu([model]);
|
||||
const contextModel = toResolverContextModel(model);
|
||||
|
||||
assert.deepEqual(
|
||||
menu.submenu.options.map(option => option.content),
|
||||
['Show in Model Resolver', 'Show info', 'Open containing folder']
|
||||
);
|
||||
assert.equal(contextModel.name, 'model.gguf');
|
||||
assert.equal(contextModel.path, model.full_path);
|
||||
assert.equal(contextModel.resolved_path, model.full_path);
|
||||
});
|
||||
|
||||
test('workflow model reference matching distinguishes widget, path, and subgraph scope', () => {
|
||||
const model = {
|
||||
node_id: 11,
|
||||
widget_index: 3,
|
||||
original_path: 'Folder\\Model.safetensors',
|
||||
is_top_level: false,
|
||||
subgraph_id: 'subgraph-a',
|
||||
};
|
||||
|
||||
assert.equal(matchesWorkflowModelReference(model, {
|
||||
node_id: '11',
|
||||
widget_index: 3,
|
||||
original_path: 'folder/model.safetensors',
|
||||
is_top_level: false,
|
||||
subgraph_id: 'subgraph-a',
|
||||
}), true);
|
||||
assert.equal(matchesWorkflowModelReference(model, {
|
||||
node_id: 11,
|
||||
widget_index: 4,
|
||||
}), false);
|
||||
assert.equal(matchesWorkflowModelReference(model, {
|
||||
node_id: 11,
|
||||
widget_index: 3,
|
||||
subgraph_id: 'subgraph-b',
|
||||
}), false);
|
||||
});
|
||||
|
||||
test('node context integration preserves existing menu hooks and refreshes after widget changes', () => {
|
||||
const configureNodeContextMenu = eval(`(${extractMethod(modelResolverSource, 'configureNodeContextMenu')})`);
|
||||
const calls = [];
|
||||
class NodeType {}
|
||||
NodeType.prototype.getExtraMenuOptions = function(_canvas, options) {
|
||||
calls.push('original-menu');
|
||||
options.push({ content: 'Original action' });
|
||||
return options;
|
||||
};
|
||||
NodeType.prototype.onWidgetChanged = function(name) {
|
||||
calls.push(`original-widget:${name}`);
|
||||
return 'widget-result';
|
||||
};
|
||||
|
||||
const model = {
|
||||
node_id: 7,
|
||||
widget_index: 0,
|
||||
widget_name: 'ckpt_name',
|
||||
original_path: 'model.safetensors',
|
||||
full_path: 'E:\\models\\model.safetensors',
|
||||
category: 'checkpoints',
|
||||
exists: true,
|
||||
};
|
||||
const resolver = {
|
||||
getResolvedModelsForNodeContextMenu: () => [model],
|
||||
scheduleNodeContextMenuAnalysis: () => calls.push('refresh'),
|
||||
showResolvedNodeModelInResolver: () => {},
|
||||
dialog: {
|
||||
getCategoryDisplayName: () => 'checkpoint',
|
||||
showModelInfo: () => {},
|
||||
openContainingFolder: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
configureNodeContextMenu.call(resolver, NodeType);
|
||||
const node = new NodeType();
|
||||
const options = [];
|
||||
const result = node.getExtraMenuOptions(null, options);
|
||||
|
||||
assert.equal(result, options);
|
||||
assert.equal(options[0].title, 'Model Resolver');
|
||||
assert.equal(options[1].content, 'Original action');
|
||||
assert.equal(node.onWidgetChanged('ckpt_name'), 'widget-result');
|
||||
assert.deepEqual(calls, ['original-menu', 'original-widget:ckpt_name', 'refresh']);
|
||||
});
|
||||
|
||||
test('Local Database source ignores installed local model matches before search', () => {
|
||||
const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`);
|
||||
const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`);
|
||||
|
||||
@@ -31,15 +31,19 @@ app.registerExtension({
|
||||
setup: modelResolver.setup,
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
modelResolver.configureWorkflowDependencyMarkerNodeType(nodeType, nodeData);
|
||||
modelResolver.configureNodeContextMenu(nodeType);
|
||||
},
|
||||
nodeCreated(node) {
|
||||
modelResolver.configureWorkflowDependencyMarkerNode(node);
|
||||
modelResolver.scheduleNodeContextMenuAnalysis();
|
||||
},
|
||||
loadedGraphNode(node) {
|
||||
modelResolver.configureWorkflowDependencyMarkerNode(node);
|
||||
modelResolver.scheduleNodeContextMenuAnalysis();
|
||||
},
|
||||
afterConfigureGraph() {
|
||||
modelResolver.configureWorkflowDependencyMarkerNodes();
|
||||
modelResolver.checkAndOpenForMissingModels();
|
||||
modelResolver.scheduleNodeContextMenuAnalysis(500);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,11 @@ import { logger as frontendLogger } from "../log_system/logger.js";
|
||||
import { loadStylesWhenNeeded } from "../utils/css_loader.js";
|
||||
import { ResolverManagerDialog } from "./resolver_dialog.js";
|
||||
import { showNotification } from "../utils/notification_utils.js";
|
||||
import {
|
||||
buildModelResolverNodeMenu,
|
||||
getResolvedModelsForNode,
|
||||
toResolverContextModel,
|
||||
} from "./node_context_menu.js";
|
||||
|
||||
const log = createModuleLogger('model_resolver');
|
||||
export const MODEL_RESOLVER_OPEN_COMMAND_ID = "ModelResolver.OpenModelResolver";
|
||||
@@ -112,6 +117,10 @@ export class ModelResolver {
|
||||
this.workflowHashMetadataSignature = null;
|
||||
this.workflowHashMetadataRefreshTimer = null;
|
||||
this.workflowHashMetadataRefreshing = false;
|
||||
this.nodeContextAnalysisData = null;
|
||||
this.nodeContextAnalysisSignature = null;
|
||||
this.nodeContextAnalysisPromise = null;
|
||||
this.nodeContextAnalysisTimer = null;
|
||||
}
|
||||
|
||||
setup = async () => {
|
||||
@@ -310,6 +319,219 @@ export class ModelResolver {
|
||||
this.handleResolverButtonClick();
|
||||
}
|
||||
|
||||
getNodeContextWorkflowState() {
|
||||
const workflow = app?.graph?.serialize?.();
|
||||
if (!workflow) return { workflow: null, signature: null };
|
||||
|
||||
const signature = this.dialog?.getWorkflowSignature?.(workflow)
|
||||
|| JSON.stringify(workflow);
|
||||
return { workflow, signature };
|
||||
}
|
||||
|
||||
getNodeContextScope(node) {
|
||||
const graph = node?.graph;
|
||||
const isTopLevel = !graph || graph === app?.graph;
|
||||
if (isTopLevel) {
|
||||
return { is_top_level: true };
|
||||
}
|
||||
|
||||
const subgraphId = graph?._subgraph?.id
|
||||
|| graph?.subgraph?.id
|
||||
|| graph?._id
|
||||
|| graph?.id
|
||||
|| '';
|
||||
return {
|
||||
is_top_level: false,
|
||||
subgraph_id: subgraphId ? String(subgraphId) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentNodeContextAnalysis(signature) {
|
||||
if (
|
||||
signature
|
||||
&& this.dialog?.cachedWorkflowSignature === signature
|
||||
&& this.dialog?.cachedAnalysisData
|
||||
) {
|
||||
return this.dialog.cachedAnalysisData;
|
||||
}
|
||||
if (
|
||||
signature
|
||||
&& this.nodeContextAnalysisSignature === signature
|
||||
&& this.nodeContextAnalysisData
|
||||
) {
|
||||
return this.nodeContextAnalysisData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
scheduleNodeContextMenuAnalysis(delay = 350) {
|
||||
if (this.nodeContextAnalysisTimer) {
|
||||
clearTimeout(this.nodeContextAnalysisTimer);
|
||||
}
|
||||
this.nodeContextAnalysisTimer = setTimeout(() => {
|
||||
this.nodeContextAnalysisTimer = null;
|
||||
this.refreshNodeContextMenuAnalysis();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
async refreshNodeContextMenuAnalysis({ force = false } = {}) {
|
||||
if (!this.dialog) return null;
|
||||
|
||||
const { workflow, signature } = this.getNodeContextWorkflowState();
|
||||
if (!workflow || !signature) return null;
|
||||
|
||||
const cachedData = this.getCurrentNodeContextAnalysis(signature);
|
||||
if (!force && cachedData) return cachedData;
|
||||
if (this.nodeContextAnalysisPromise) return this.nodeContextAnalysisPromise;
|
||||
|
||||
const analysisPromise = this.dialog.fetchJson('/model_resolver/analyze', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ workflow }),
|
||||
silent: true,
|
||||
}, 'Analyze workflow')
|
||||
.then((data) => {
|
||||
const currentState = this.getNodeContextWorkflowState();
|
||||
if (currentState.signature === signature) {
|
||||
this.nodeContextAnalysisSignature = signature;
|
||||
this.nodeContextAnalysisData = data;
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
log.debug('Model Resolver: node context analysis failed.', error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.nodeContextAnalysisPromise === analysisPromise) {
|
||||
this.nodeContextAnalysisPromise = null;
|
||||
}
|
||||
const currentState = this.getNodeContextWorkflowState();
|
||||
if (currentState.signature && currentState.signature !== signature) {
|
||||
this.scheduleNodeContextMenuAnalysis(0);
|
||||
}
|
||||
});
|
||||
|
||||
this.nodeContextAnalysisPromise = analysisPromise;
|
||||
return analysisPromise;
|
||||
}
|
||||
|
||||
getResolvedModelsForNodeContextMenu(node) {
|
||||
const { signature } = this.getNodeContextWorkflowState();
|
||||
const data = this.getCurrentNodeContextAnalysis(signature);
|
||||
if (!data) {
|
||||
this.scheduleNodeContextMenuAnalysis(0);
|
||||
return [];
|
||||
}
|
||||
|
||||
return getResolvedModelsForNode(
|
||||
data,
|
||||
node?.id,
|
||||
this.getNodeContextScope(node)
|
||||
);
|
||||
}
|
||||
|
||||
configureNodeContextMenu(nodeType) {
|
||||
if (!nodeType?.prototype || nodeType.prototype.__modelResolverContextMenuPatched) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this;
|
||||
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
const originalOnWidgetChanged = nodeType.prototype.onWidgetChanged;
|
||||
nodeType.prototype.getExtraMenuOptions = function(_, options) {
|
||||
const result = originalGetExtraMenuOptions?.apply(this, arguments);
|
||||
const models = owner.getResolvedModelsForNodeContextMenu(this);
|
||||
const menu = buildModelResolverNodeMenu(models, {
|
||||
showInResolver: model => owner.showResolvedNodeModelInResolver(model),
|
||||
showInfo: model => owner.dialog?.showModelInfo(toResolverContextModel(model)),
|
||||
openContainingFolder: model => owner.dialog?.openContainingFolder(toResolverContextModel(model)),
|
||||
}, {
|
||||
formatCategory: category => owner.dialog?.getCategoryDisplayName?.(category),
|
||||
});
|
||||
|
||||
const targetOptions = Array.isArray(options)
|
||||
? options
|
||||
: (Array.isArray(result) ? result : null);
|
||||
if (menu && targetOptions) {
|
||||
targetOptions.unshift(menu);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
nodeType.prototype.onWidgetChanged = function() {
|
||||
const result = originalOnWidgetChanged?.apply(this, arguments);
|
||||
owner.scheduleNodeContextMenuAnalysis();
|
||||
return result;
|
||||
};
|
||||
nodeType.prototype.__modelResolverContextMenuPatched = true;
|
||||
}
|
||||
|
||||
waitForResolverDialogReady(timeoutMs = 2500) {
|
||||
const startedAt = Date.now();
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (this.dialog?.isVisible?.() && this.dialog?.contentElement) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt >= timeoutMs) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
setTimeout(check, 40);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
async showResolvedNodeModelInResolver(reference) {
|
||||
if (!this.dialog) return;
|
||||
|
||||
const { signature } = this.getNodeContextWorkflowState();
|
||||
const analysisData = this.getCurrentNodeContextAnalysis(signature);
|
||||
if (signature && analysisData) {
|
||||
this.dialog.cachedWorkflowSignature = signature;
|
||||
this.dialog.cachedAnalysisData = analysisData;
|
||||
}
|
||||
|
||||
this.dialog.activeTab = 'missing';
|
||||
this.dialog.persistActiveTab?.('missing');
|
||||
this.dialog.showResolvedModels = true;
|
||||
this.dialog.missingModelsTypeFilter = 'all';
|
||||
this.dialog.missingModelsTypeFilterMenuOpen = false;
|
||||
|
||||
try {
|
||||
localStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1');
|
||||
} catch (error) {
|
||||
log.debug('Model Resolver: failed to persist resolved-model visibility.', error);
|
||||
}
|
||||
|
||||
if (!this.dialog.isVisible?.()) {
|
||||
this.activateResolverButton();
|
||||
}
|
||||
if (!await this.waitForResolverDialogReady()) {
|
||||
showNotification('Could not open Model Resolver.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog.switchTab?.('missing', { force: true });
|
||||
await this.dialog.loadWorkflowData?.();
|
||||
|
||||
let selected = this.dialog.selectWorkflowModelReference?.(
|
||||
reference,
|
||||
this.dialog.cachedAnalysisData
|
||||
);
|
||||
if (!selected) {
|
||||
await this.dialog.loadWorkflowData?.(null, { force: true, forceRescan: false });
|
||||
selected = this.dialog.selectWorkflowModelReference?.(
|
||||
reference,
|
||||
this.dialog.cachedAnalysisData
|
||||
);
|
||||
}
|
||||
if (!selected) {
|
||||
showNotification('The selected model is no longer present in the current workflow.', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
handleSidebarButtonClick(event) {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest(`.${this.sidebarTabId}-tab-button`);
|
||||
@@ -1114,15 +1336,9 @@ export class ModelResolver {
|
||||
}
|
||||
this.lastCheckedWorkflow = workflowHash;
|
||||
|
||||
// Call analyze endpoint to check for missing models
|
||||
let data;
|
||||
try {
|
||||
data = await this.dialog.fetchJson('/model_resolver/analyze', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ workflow }),
|
||||
silent: true
|
||||
}, 'Analyze workflow');
|
||||
} catch (error) {
|
||||
// Reuse the background context-menu analysis to avoid scanning twice.
|
||||
const data = await this.refreshNodeContextMenuAnalysis();
|
||||
if (!data) {
|
||||
console.warn('Model Resolver: Failed to analyze workflow for missing models');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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 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 buildModelResolverNodeMenu(models = [], handlers = {}, options = {}) {
|
||||
const resolvedModels = models.filter(isExistingResolvedModel);
|
||||
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',
|
||||
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',
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js";
|
||||
import { startSplitterDrag } from "../utils/splitter_drag.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
|
||||
import { matchesWorkflowModelReference } from "../node_context_menu.js";
|
||||
|
||||
export const missingBrowserMethods = {
|
||||
getMissingFilename(missing = {}) {
|
||||
@@ -507,6 +508,27 @@ export const missingBrowserMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
selectWorkflowModelReference(reference = {}, data = this.cachedAnalysisData || {}) {
|
||||
const selected = this.getResolvedWorkflowModels(data)
|
||||
.find(model => matchesWorkflowModelReference(model, reference));
|
||||
if (!selected || !this.contentElement) return null;
|
||||
|
||||
this.showResolvedModels = true;
|
||||
this.missingModelsTypeFilter = 'all';
|
||||
this.missingModelsTypeFilterMenuOpen = false;
|
||||
safeStorage.setItem(this.showResolvedModelsStorageKey, '1');
|
||||
this.selectedMissingModelKey = this.getMissingModelKey(selected);
|
||||
this.displayMissingModels(this.contentElement, data);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const row = Array.from(
|
||||
this.contentElement?.querySelectorAll?.('.mr-missing-list-row') || []
|
||||
).find(item => item.dataset.missingKey === this.selectedMissingModelKey);
|
||||
row?.scrollIntoView?.({ block: 'nearest' });
|
||||
});
|
||||
return selected;
|
||||
},
|
||||
|
||||
isAutoDownloadModel(missing = {}) {
|
||||
return Boolean(missing?.auto_download_capable || missing?.auto_download_candidate);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user