Add locate-node context action for loaded models with subgraph awareness
This commit is contained in:
+76
-1
@@ -1796,6 +1796,15 @@ class ModelResolverExtension:
|
||||
# Also extract from node.properties.models
|
||||
workflow_nodes = workflow_json.get("nodes", [])
|
||||
nodes = list(workflow_nodes) if isinstance(workflow_nodes, list) else []
|
||||
node_scopes = {
|
||||
id(node): {
|
||||
"subgraph_id": "",
|
||||
"subgraph_name": "",
|
||||
"is_top_level": True,
|
||||
}
|
||||
for node in nodes
|
||||
if isinstance(node, dict)
|
||||
}
|
||||
definitions = workflow_json.get("definitions", {})
|
||||
subgraphs = (
|
||||
definitions.get("subgraphs", [])
|
||||
@@ -1807,6 +1816,44 @@ class ModelResolverExtension:
|
||||
subgraph_nodes = subgraph.get("nodes", [])
|
||||
if isinstance(subgraph_nodes, list):
|
||||
nodes.extend(subgraph_nodes)
|
||||
for node in subgraph_nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
node_scopes[id(node)] = {
|
||||
"subgraph_id": subgraph.get("id") or "",
|
||||
"subgraph_name": (
|
||||
subgraph.get("name")
|
||||
or subgraph.get("id")
|
||||
or ""
|
||||
),
|
||||
"is_top_level": False,
|
||||
}
|
||||
|
||||
def get_node_scope(node):
|
||||
return node_scopes.get(
|
||||
id(node),
|
||||
{
|
||||
"subgraph_id": "",
|
||||
"subgraph_name": "",
|
||||
"is_top_level": True,
|
||||
},
|
||||
)
|
||||
|
||||
def node_matches_ref(node, ref):
|
||||
if not isinstance(node, dict):
|
||||
return False
|
||||
if str(node.get("id")) != str(ref.get("node_id")):
|
||||
return False
|
||||
scope = get_node_scope(node)
|
||||
if scope["is_top_level"] != (
|
||||
ref.get("is_top_level") is not False
|
||||
):
|
||||
return False
|
||||
if not scope["is_top_level"]:
|
||||
return str(scope["subgraph_id"]) == str(
|
||||
ref.get("subgraph_id") or ""
|
||||
)
|
||||
return True
|
||||
|
||||
# Collect all loaded models with their values
|
||||
loaded_models = []
|
||||
@@ -1847,7 +1894,7 @@ class ModelResolverExtension:
|
||||
):
|
||||
# Find the node in workflow to get strength value
|
||||
for node in nodes:
|
||||
if str(node.get("id")) == str(node_id):
|
||||
if node_matches_ref(node, ref):
|
||||
widgets_values = node.get("widgets_values", [])
|
||||
if (
|
||||
isinstance(widgets_values, list)
|
||||
@@ -1890,6 +1937,25 @@ class ModelResolverExtension:
|
||||
"node_id": node_id,
|
||||
"widget_index": widget_index,
|
||||
"node_type": node_type,
|
||||
"node_title": ref.get("node_title", ""),
|
||||
"subgraph_id": ref.get("subgraph_id") or "",
|
||||
"subgraph_name": ref.get("subgraph_name") or "",
|
||||
"is_top_level": ref.get("is_top_level") is not False,
|
||||
"locate_node_id": ref.get("locate_node_id"),
|
||||
"locate_node_type": ref.get("locate_node_type", ""),
|
||||
"locate_node_title": ref.get("locate_node_title", ""),
|
||||
"locate_subgraph_id": (
|
||||
ref.get("locate_subgraph_id") or ""
|
||||
),
|
||||
"locate_subgraph_name": (
|
||||
ref.get("locate_subgraph_name") or ""
|
||||
),
|
||||
"locate_is_top_level": (
|
||||
ref.get("locate_is_top_level") is not False
|
||||
),
|
||||
"locate_via_promoted_widget": ref.get(
|
||||
"locate_via_promoted_widget", False
|
||||
),
|
||||
"exists": exists,
|
||||
"strength": strength,
|
||||
"original_path": original_path,
|
||||
@@ -1932,6 +1998,7 @@ class ModelResolverExtension:
|
||||
if isinstance(model_info, dict):
|
||||
name = model_info.get("name", "")
|
||||
directory = model_info.get("directory", "")
|
||||
node_scope = get_node_scope(node)
|
||||
|
||||
if name:
|
||||
# Check if this model is already in loaded_models
|
||||
@@ -1951,6 +2018,14 @@ class ModelResolverExtension:
|
||||
"node_id": node.get("id"),
|
||||
"widget_index": None,
|
||||
"node_type": node_type,
|
||||
"node_title": node.get("title", ""),
|
||||
"subgraph_id": node_scope["subgraph_id"],
|
||||
"subgraph_name": node_scope[
|
||||
"subgraph_name"
|
||||
],
|
||||
"is_top_level": node_scope[
|
||||
"is_top_level"
|
||||
],
|
||||
"exists": True, # Embedded models are loaded
|
||||
"strength": None,
|
||||
"original_path": name,
|
||||
|
||||
@@ -46,6 +46,14 @@ const missingBrowserMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/views/missing_browser_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const tabsLoadedMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/views/tabs_loaded_methods.js'),
|
||||
'utf8'
|
||||
);
|
||||
const resolverDialogSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/resolver_dialog.js'),
|
||||
'utf8'
|
||||
);
|
||||
const renderFormatMethodsSource = fs.readFileSync(
|
||||
path.join(projectRoot, 'web/resolver/utils/render_format_methods.js'),
|
||||
'utf8'
|
||||
@@ -328,6 +336,44 @@ test('pending node context model resolves against the current workflow analysis
|
||||
assert.equal(isExistingResolvedModel(resolvedModel), true);
|
||||
});
|
||||
|
||||
test('loaded model context menu locates its exact workflow node and subgraph', () => {
|
||||
const getLoadedModelContext = eval(`(${extractMethod(tabsLoadedMethodsSource, 'getLoadedModelContext')})`);
|
||||
const handleContextMenuAction = eval(`(${extractMethod(modelInfoMethodsSource, 'handleContextMenuAction')})`);
|
||||
const calls = [];
|
||||
const model = getLoadedModelContext.call({}, {
|
||||
node_id: 19,
|
||||
subgraph_id: 'subgraph-a',
|
||||
is_top_level: false,
|
||||
});
|
||||
const dialog = {
|
||||
_contextMenuModel: model,
|
||||
hideContextMenu() {},
|
||||
getMissingLocateTarget(value) {
|
||||
return {
|
||||
nodeId: value.node_id,
|
||||
subgraphId: value.subgraph_id,
|
||||
isTopLevel: value.is_top_level,
|
||||
};
|
||||
},
|
||||
locateNodeInGraph(nodeId, options) {
|
||||
calls.push({ nodeId, options });
|
||||
},
|
||||
};
|
||||
|
||||
handleContextMenuAction.call(dialog, 'locateNode');
|
||||
|
||||
assert.equal(model.context_scope, 'loaded_model');
|
||||
assert.deepEqual(calls, [{
|
||||
nodeId: 19,
|
||||
options: {
|
||||
subgraphId: 'subgraph-a',
|
||||
isTopLevel: false,
|
||||
},
|
||||
}]);
|
||||
assert.match(resolverDialogSource, /data-menu-action": "locateNode"/);
|
||||
assert.match(resolverDialogSource, /textContent: "Locate Node"/);
|
||||
});
|
||||
|
||||
test('queued workflow model selection survives missing data and completes after analysis', () => {
|
||||
const queueWorkflowModelReferenceSelection = eval(`(${extractMethod(missingBrowserMethodsSource, 'queueWorkflowModelReferenceSelection')})`);
|
||||
const applyPendingWorkflowModelSelection = eval(`(${extractMethod(missingBrowserMethodsSource, 'applyPendingWorkflowModelSelection')})`);
|
||||
|
||||
@@ -149,6 +149,15 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
$el("div.mr-context-menu-divider.mr-context-menu-divider-workflow", {
|
||||
"data-menu-divider": "workflow"
|
||||
}),
|
||||
$el("div.mr-context-menu-item.mr-context-menu-action-locate-node", {
|
||||
"data-menu-action": "locateNode",
|
||||
onclick: () => this.handleContextMenuAction('locateNode')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", {
|
||||
innerHTML: getSvgIcon('locate', 'currentColor', 'mr-context-menu-item-svg')
|
||||
}),
|
||||
$el("span", { textContent: "Locate Node" })
|
||||
]),
|
||||
$el("div.mr-context-menu-item.mr-context-menu-action-switch-workflow", {
|
||||
"data-menu-action": "switchWorkflow",
|
||||
onclick: () => this.handleContextMenuAction('switchWorkflow')
|
||||
|
||||
@@ -37,6 +37,7 @@ export const modelInfoMethods = {
|
||||
const isDownloadRootContext = model?.context_scope === 'download_root';
|
||||
const isDownloadQueueContext = model?.context_scope === 'download_queue';
|
||||
const isDownloadHistoryContext = model?.context_scope === 'download_history';
|
||||
const isLoadedModelContext = model?.context_scope === 'loaded_model';
|
||||
const isLocalModelContext = model?.context_scope === 'local_model' || model?.context_scope === 'local_match';
|
||||
const isFolderOnlyContext = isDownloadFolderContext || isDownloadRootContext;
|
||||
const isSourceModelContext = !isDownloadTableContext && !isFolderOnlyContext && !isDownloadQueueContext && !isDownloadHistoryContext;
|
||||
@@ -46,15 +47,23 @@ export const modelInfoMethods = {
|
||||
&& hasLocalPath
|
||||
&& Boolean(model?.missing_key || model?.missing_search_key);
|
||||
const showSwitchWorkflow = (isDownloadQueueContext || isDownloadHistoryContext) && Boolean(this.canSwitchToDownloadWorkflow?.(model));
|
||||
const showLocateNode = isLoadedModelContext
|
||||
&& model?.node_id !== undefined
|
||||
&& model?.node_id !== null
|
||||
&& model?.node_id !== '';
|
||||
this.setContextMenuItemVisible('showInfo', isSourceModelContext);
|
||||
this.setContextMenuItemVisible('showMore', canShowMore);
|
||||
this.setContextMenuItemVisible('source', showSourceLink);
|
||||
this.setContextMenuItemVisible('civitai', false);
|
||||
this.setContextMenuItemVisible('locateNode', showLocateNode);
|
||||
this.setContextMenuItemVisible('switchWorkflow', showSwitchWorkflow);
|
||||
this.setContextMenuItemVisible('compareHashes', showCompareHashes);
|
||||
this.setContextMenuItemVisible('openFolder', showOpenFolder);
|
||||
this.setContextMenuDividerVisible('source', showSourceLink && (isSourceModelContext || canShowMore));
|
||||
this.setContextMenuDividerVisible('workflow', showSwitchWorkflow && (isSourceModelContext || canShowMore || showSourceLink));
|
||||
this.setContextMenuDividerVisible(
|
||||
'workflow',
|
||||
(showLocateNode || showSwitchWorkflow) && (isSourceModelContext || canShowMore || showSourceLink)
|
||||
);
|
||||
this.setContextMenuDividerVisible('folder', (showCompareHashes || showOpenFolder) && (isSourceModelContext || canShowMore || showSourceLink || showSwitchWorkflow));
|
||||
this.updateContextMenuSourceItem(sourceLink);
|
||||
|
||||
@@ -107,6 +116,12 @@ export const modelInfoMethods = {
|
||||
this.compareLocalModelHashesWithCurrentFinding(model);
|
||||
} else if (action === 'switchWorkflow') {
|
||||
this.switchToDownloadWorkflow(model);
|
||||
} else if (action === 'locateNode') {
|
||||
const locateTarget = this.getMissingLocateTarget(model);
|
||||
this.locateNodeInGraph(locateTarget.nodeId, {
|
||||
subgraphId: locateTarget.subgraphId || '',
|
||||
isTopLevel: locateTarget.isTopLevel
|
||||
});
|
||||
} else if (action === 'showInfo') {
|
||||
this.showModelInfo(model);
|
||||
} else if (action === 'showMore') {
|
||||
|
||||
@@ -176,6 +176,13 @@ export const tabsLoadedMethods = {
|
||||
return `<${this.getCategoryTokenName(category)}:${name}:${strength || '1.00'}>`;
|
||||
},
|
||||
|
||||
getLoadedModelContext(model = {}) {
|
||||
return {
|
||||
...model,
|
||||
context_scope: 'loaded_model'
|
||||
};
|
||||
},
|
||||
|
||||
displayLoadedModels(container, data) {
|
||||
const loadedModels = data.loaded_models || [];
|
||||
const total = data.total || 0;
|
||||
@@ -274,7 +281,7 @@ export const tabsLoadedMethods = {
|
||||
|
||||
for (const model of modelsObj.active) {
|
||||
const { name, strength } = this.getModelNameAndStrength(model);
|
||||
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(model)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(this.getLoadedModelContext(model))}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
@@ -291,7 +298,7 @@ export const tabsLoadedMethods = {
|
||||
|
||||
for (const model of modelsObj.inactive) {
|
||||
const { name, strength } = this.getModelNameAndStrength(model);
|
||||
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(model)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
html += `<span class="mr-model-chip"${this.getContextMenuAttrs(this.getLoadedModelContext(model))}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user