From 3829f959600d9283ee8ce4f0d0ef5a5ac5fbec07 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 17 Sep 2026 00:48:00 +0200 Subject: [PATCH] Improve image metadata transfer guidance and prompt candidate ordering --- tests/test_image_metadata_transfer.mjs | 12 ++++ tests/test_prompt_transfer.mjs | 57 ++++++++++++++++++ web/css/resolver-main.css | 23 +++++++- web/resolver/utils/prompt_utils.js | 51 ++++++++++++++++ .../views/tabs_image_metadata_methods.js | 58 ++++++++++++++----- 5 files changed, 185 insertions(+), 16 deletions(-) diff --git a/tests/test_image_metadata_transfer.mjs b/tests/test_image_metadata_transfer.mjs index d8bdbb4..6aefecf 100644 --- a/tests/test_image_metadata_transfer.mjs +++ b/tests/test_image_metadata_transfer.mjs @@ -214,6 +214,18 @@ test('metadata transfer shows a spinner while applying', () => { assert.match(html, /Applying\.\.\.<\/span>/); }); +test('metadata transfer guidance is available through a question tooltip', () => { + const context = createTransferContext(); + const state = createTransferState([]); + state.loadedModels = null; + + const html = context.renderMetadataTransferPanel(state); + + assert.match(html, /class="mr-tooltip-badge"[^>]*data-tooltip="[\s\S]*Select a node row to include it in the transfer/); + assert.match(html, /Import a workflow with model metadata to enable Apply transfer/); + assert.doesNotMatch(html, /class="mr-image-transfer-note"/); +}); + test('metadata transfer keeps target node checkboxes selectable without an imported workflow', () => { const context = createTransferContext(); const state = createTransferState([]); diff --git a/tests/test_prompt_transfer.mjs b/tests/test_prompt_transfer.mjs index 5059b67..1edc877 100644 --- a/tests/test_prompt_transfer.mjs +++ b/tests/test_prompt_transfer.mjs @@ -3,6 +3,8 @@ import assert from 'node:assert/strict'; import { imageMetadataMethods } from '../web/resolver/views/tabs_image_metadata_methods.js'; import { extractWorkflowPrompts, + getPromptCandidateScore, + sortPromptCandidates, updateWorkflowPromptValue, } from '../web/resolver/utils/prompt_utils.js'; @@ -83,6 +85,33 @@ test('workflow prompt extraction distinguishes positive and negative CLIP text n ); }); +test('prompt candidates are ranked by role and prompt-specific node type', () => { + const candidates = [ + { + id: 'generic', + node_type: 'PrimitiveStringMultiline', + node_title: 'Text value', + widget_name: 'value', + role: 'unknown', + role_candidates: [], + }, + { + id: 'clip', + node_type: 'CLIPTextEncode', + node_title: '', + widget_name: 'text', + role: 'positive', + role_candidates: ['positive'], + }, + ]; + + assert.ok(getPromptCandidateScore(candidates[1], 'positive') > getPromptCandidateScore(candidates[0], 'positive')); + assert.deepEqual( + sortPromptCandidates(candidates, 'positive').map(candidate => candidate.id), + ['clip', 'generic'], + ); +}); + test('workflow prompt extraction inherits a role for a plain text node feeding CLIPTextEncode', () => { const workflow = { nodes: [ @@ -344,6 +373,8 @@ test('metadata prompt panel renders separate positive and negative transfer rows assert.match(html, /data-image-prompt-target="negative"/); assert.match(html, /data-image-prompt-role-selection="positive"[^>]*checked/); assert.match(html, /data-image-prompt-role-selection="negative"[^>]*checked/); + assert.match(html, /class="mr-tooltip-badge"[^>]*data-tooltip="[\s\S]*Select which prompt roles to replace/); + assert.doesNotMatch(html, /class="mr-image-prompt-note"/); const positiveTargetIndex = html.indexOf('data-image-prompt-target="positive"'); const positiveSourceIndex = html.indexOf('data-image-prompt-source="positive"'); assert.ok(positiveTargetIndex >= 0 && positiveTargetIndex < positiveSourceIndex); @@ -355,6 +386,32 @@ test('metadata prompt panel renders separate positive and negative transfer rows assert.match(html, /Replace prompts/); }); +test('metadata prompt selectors put the most likely prompt node first', () => { + const currentWorkflow = { + nodes: [ + { + id: 90, + type: 'PrimitiveStringMultiline', + widgets_values: ['generic text field'], + }, + clipTextNode(371, 'actual prompt', 11), + { + id: 3, + type: 'KSampler', + inputs: [{ name: 'positive', type: 'CONDITIONING', link: 11 }], + }, + ], + }; + const context = createContext(createRoleWorkflow(), currentWorkflow); + const positiveRow = context.getMetadataPromptTransferRows(context.imageInspectorState) + .find(row => row.role === 'positive'); + + assert.deepEqual( + positiveRow.targetOptions.map(prompt => prompt.node_id), + [371, 90], + ); +}); + test('metadata prompt role checkboxes stay selectable without an imported workflow', () => { const context = createContext(null, createRoleWorkflow('current positive', 'current negative')); context.imageInspectorState.metadata = null; diff --git a/web/css/resolver-main.css b/web/css/resolver-main.css index 73eb8bf..aaf6293 100644 --- a/web/css/resolver-main.css +++ b/web/css/resolver-main.css @@ -2151,6 +2151,11 @@ color: #101010; } +#model-resolver-modal [data-image-inspector-action="apply-prompts"].mr-btn-primary, +#model-resolver-modal [data-image-inspector-action="apply-prompts"].mr-btn-primary:hover:not(:disabled) { + color: #101010; +} + .mr-btn-secondary, .mr-btn-link, #model-resolver-modal .mr-btn-secondary, @@ -4233,6 +4238,10 @@ margin: 0; } +.mr-image-inspector .mr-loaded-models-title { + font-size: var(--mr-font-xl); +} + .mr-loaded-total { display: inline-flex; align-items: center; @@ -4972,7 +4981,19 @@ button.mr-info-link:disabled { } .mr-image-prompt-row.is-selected { - background: rgba(114, 211, 138, 0.055); + position: relative; + background: linear-gradient(90deg, rgba(114, 211, 138, 0.12), rgba(114, 211, 138, 0.035) 55%, transparent); +} + +.mr-image-prompt-row.is-selected::before { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 3px; + border-radius: 0 2px 2px 0; + background: var(--mr-accent); + content: ""; } .mr-image-prompt-row-header { diff --git a/web/resolver/utils/prompt_utils.js b/web/resolver/utils/prompt_utils.js index 6364f7c..8402ae7 100644 --- a/web/resolver/utils/prompt_utils.js +++ b/web/resolver/utils/prompt_utils.js @@ -18,6 +18,11 @@ const PROMPT_WIDGET_NAMES = new Set([ ]); const PROMPT_NODE_TYPE_PATTERN = /(?:clip.*text|text.*encode|primitive.*string|string.*multiline|text.*multiline|multiline.*text|conditioning.*text|textinput)/i; +const PROMPT_NODE_TYPE_SCORE_RULES = [ + { pattern: /(?:clip.*text|text.*encode)/i, score: 500 }, + { pattern: /conditioning.*text/i, score: 420 }, + { pattern: /(?:primitive.*string|string.*multiline|text.*multiline|multiline.*text|textinput)/i, score: 320 }, +]; const SCALAR_WIDGET_TYPES = new Set(['STRING', 'INT', 'FLOAT', 'BOOLEAN']); const PROMPT_INPUT_NAMES = new Set(['text', 'prompt', 'text_input', 'string']); const TEXT_WIDGET_TYPES = new Set(['STRING', 'TEXT']); @@ -430,6 +435,52 @@ function isPromptTextDescriptor(descriptor = {}, { node = {}, hasGraphPromptRole )); } +function getPromptLabelScore(value, role = '') { + const label = String(value ?? '').trim(); + const token = normalizePromptName(label); + if (!token) return 0; + + let score = 0; + if (role && getRoleFromLabel(label) === role) score += 180; + if (token.includes('prompt')) score += 110; + if (token === 'text' || token.includes('text')) score += 35; + return score; +} + +function getPromptNodeTypeScore(value) { + const nodeType = String(value ?? '').trim(); + return PROMPT_NODE_TYPE_SCORE_RULES.find(rule => rule.pattern.test(nodeType))?.score || 0; +} + +export function getPromptCandidateScore(prompt = {}, role = '') { + const requestedRole = String(role || '').trim(); + const roleCandidates = Array.isArray(prompt.role_candidates) + ? prompt.role_candidates + : []; + let score = 0; + + if (requestedRole && prompt.role === requestedRole) score += 1000; + else if (requestedRole && roleCandidates.includes(requestedRole)) score += 650; + + score += getPromptNodeTypeScore(prompt.node_type); + score += getPromptLabelScore(prompt.node_title, requestedRole); + score += getPromptLabelScore(prompt.widget_name, requestedRole); + return score; +} + +export function sortPromptCandidates(prompts = [], role = '') { + if (!Array.isArray(prompts)) return []; + + return prompts + .map((prompt, index) => ({ + prompt, + index, + score: getPromptCandidateScore(prompt, role), + })) + .sort((left, right) => right.score - left.score || left.index - right.index) + .map(({ prompt }) => prompt); +} + function promptIdentity(entry, descriptor) { return `${entry.graphKey}:${String(entry.node.id)}:${descriptor.index}:${normalizePromptName(descriptor.name)}`; } diff --git a/web/resolver/views/tabs_image_metadata_methods.js b/web/resolver/views/tabs_image_metadata_methods.js index 5e1d3ec..3cdefff 100644 --- a/web/resolver/views/tabs_image_metadata_methods.js +++ b/web/resolver/views/tabs_image_metadata_methods.js @@ -7,6 +7,7 @@ import { normalizeCategoryValue } from "../utils/category_utils.js"; import { extractWorkflowPrompts, getPromptRoleLabel, + sortPromptCandidates, updateWorkflowPromptValue, } from "../utils/prompt_utils.js"; @@ -202,8 +203,7 @@ export const imageMetadataMethods = {
-

Image Metadata

-

Paste JSON or drop a supported file anywhere in this panel.

+

Image Metadata ${this.getMetadataTooltipBadge('Paste JSON or drop a supported file anywhere in this panel.', 'Image metadata help')}

@@ -225,6 +225,7 @@ export const imageMetadataMethods = { this.bindImageMetadataEvents(); } + this.bindTooltips?.(this.contentElement); this.renderImageMetadataResult(); }, @@ -725,6 +726,13 @@ export const imageMetadataMethods = { return `${text.slice(0, MAX_PROMPT_PREVIEW_LENGTH)}\n…`; }, + getMetadataTooltipBadge(text, ariaLabel = 'More information') { + const tooltip = String(text ?? '').trim(); + if (!tooltip) return ''; + const label = String(ariaLabel ?? 'More information').trim() || 'More information'; + return `?`; + }, + getMetadataPromptTransferData(state = null) { const imageState = state || getImageInspectorState(this); const promptTransfer = this.getMetadataPromptTransferState(imageState); @@ -739,12 +747,18 @@ export const imageMetadataMethods = { const usedTargetIds = new Set(); const rows = METADATA_PROMPT_ROLES.map(role => { - const sourceOptions = sourcePrompts.filter(prompt => ( - prompt.role === role || prompt.role === 'unknown' - )); - const targetOptions = targetPrompts.filter(prompt => ( - prompt.role === role || prompt.role === 'unknown' - )); + const sourceOptions = sortPromptCandidates( + sourcePrompts.filter(prompt => ( + prompt.role === role || prompt.role === 'unknown' + )), + role, + ); + const targetOptions = sortPromptCandidates( + targetPrompts.filter(prompt => ( + prompt.role === role || prompt.role === 'unknown' + )), + role, + ); const requestedSource = sourceOptions.find(prompt => ( String(prompt.id) === String(sourceSelections[role] || '') )); @@ -886,6 +900,13 @@ export const imageMetadataMethods = { const unclassifiedNote = data.unclassifiedSourceCount || data.unclassifiedTargetCount ? 'Some prompts have no explicit role in the graph and remain selectable in both slots.' : 'Positive and negative roles are detected from connected node input names whenever available.'; + const promptHelpText = [ + sourceSummary, + targetSummary, + unclassifiedNote, + 'Select which prompt roles to replace, then choose the source and target text fields.', + 'Select an imported prompt and a current text field.', + ].join('\n\n'); const statusHtml = promptTransfer.error ? `

${this.escapeHtml(promptTransfer.error)}

` : ''; @@ -894,12 +915,10 @@ export const imageMetadataMethods = {
-

Transfer prompts to current workflow

-

${this.escapeHtml(sourceSummary)} ${this.escapeHtml(targetSummary)}

+

Transfer prompts to current workflow ${this.getMetadataTooltipBadge(promptHelpText, 'Prompt transfer help')}

${statusHtml} -

${this.escapeHtml(unclassifiedNote)} Select which prompt roles to replace, then choose the source and target text fields.

${roleRows || '

No positive or negative prompt slots are available.

'}
@@ -911,7 +930,7 @@ export const imageMetadataMethods = { ? 'Select positive, negative, or both prompt roles.' : hasApplicablePair ? `${data.applicableSelectedRoleCount} prompt role${data.applicableSelectedRoleCount === 1 ? '' : 's'} selected for replacement.` - : 'Select an imported prompt and a current text field.' + : 'Prompt source and target selection needed.' )}