Refresh metadata prompt previews when workflow text changes

This commit is contained in:
Dariusz L
2026-10-05 18:27:20 +02:00
parent 6895aa83a3
commit 53a97e8a5b
5 changed files with 73 additions and 0 deletions
@@ -3002,6 +3002,36 @@ test('Missing Models filter toggles persist state and rerender once per change',
}
});
test('metadata refresh updates prompts even when the model workflow signature is unchanged', async () => {
const refresh = eval(`(${extractMethod(workflowStateMethodsSource, 'refreshForActiveWorkflowChange')})`);
const workflow = { nodes: [{ id: 1, type: 'CLIPTextEncode', widgets_values: ['edited prompt'] }] };
let promptRefreshes = 0;
const dialog = {
_workflowRefreshGeneration: 1,
activeWorkflowRouteKey: 'workflow-a',
activeWorkflowSignature: 'same-model-signature',
activeTab: 'metadata',
isVisible: () => true,
getActiveWorkflowRouteKey: () => 'workflow-a',
getCurrentWorkflow: () => workflow,
getWorkflowSignature: () => 'same-model-signature',
getMissingWorkflowSignature: () => 'same-model-signature',
refreshMetadataPromptPreview(value) {
assert.equal(value, workflow);
promptRefreshes += 1;
return true;
},
openMetadataTransfer() { assert.fail('Prompt-only edits must not rescan models'); },
syncWorkflowScopedQueue() { assert.fail('Prompt-only edits must preserve model state'); },
};
await refresh.call(dialog, { reason: 'node-widget-change', generation: 1 });
assert.equal(promptRefreshes, 1);
assert.equal(dialog._workflowRefreshRetryTimer, undefined);
dialog._applyWorkflowRefreshSuppressionDepth = 1;
await refresh.call(dialog, { reason: 'node-widget-change', generation: 1 });
assert.equal(promptRefreshes, 1);
});
test('node widget changes request a content-preserving Missing Models refresh', async () => {
const log = { debug() {} };
const refreshForActiveWorkflowChange = eval(
+22
View File
@@ -73,6 +73,28 @@ function createContext(sourceWorkflow, currentWorkflow) {
};
}
test('live prompt preview detects positive and negative text changes without changing imported metadata', () => {
const source = createRoleWorkflow('imported positive', 'imported negative');
const current = createRoleWorkflow('current positive', 'current negative');
const dialog = createContext(source, current);
let refreshes = 0;
dialog.renderMetadataPromptTransferPanelInPlace = () => {
refreshes += 1;
dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState);
return true;
};
dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState);
assert.equal(dialog.refreshMetadataPromptPreview(current), false);
current.nodes[0].widgets_values[0] = 'edited positive';
assert.equal(dialog.refreshMetadataPromptPreview(current), true);
assert.match(dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState), /edited positive/);
current.nodes[1].widgets_values[0] = 'edited negative';
assert.equal(dialog.refreshMetadataPromptPreview(current), true);
assert.equal(dialog.refreshMetadataPromptPreview(current), false);
assert.equal(refreshes, 2);
assert.equal(source.nodes[0].widgets_values[0], 'imported positive');
});
test('workflow prompt extraction distinguishes positive and negative CLIP text nodes', () => {
const prompts = extractWorkflowPrompts(createRoleWorkflow());
+10
View File
@@ -955,6 +955,15 @@ export class ModelResolver {
const focusHandler = () => {
window.__ModelResolverWorkflowChangeOwner?.handleActiveWorkflowRouteChange('window-focus');
};
const promptInputHandler = (event) => {
const dialog = window.__ModelResolverWorkflowChangeOwner?.dialog;
const target = event.target instanceof Element ? event.target : null;
if (!dialog?.isVisible() || dialog.activeTab !== 'metadata') return;
if (target?.closest('#model-resolver-modal, .model-resolver-backdrop')) return;
if (!target?.matches('textarea, input[type="text"], [contenteditable="true"]')) return;
dialog.scheduleActiveWorkflowRefresh('node-widget-change');
};
document.addEventListener('input', promptInputHandler);
const visibilityHandler = () => {
if (document.visibilityState === 'visible') {
window.__ModelResolverWorkflowChangeOwner?.handleActiveWorkflowRouteChange('visibility-change');
@@ -970,6 +979,7 @@ export class ModelResolver {
document.addEventListener('visibilitychange', visibilityHandler);
window.__ModelResolverWorkflowChangeHandlers = [
{ target: document, event: 'input', handler: promptInputHandler },
{ target: window, event: 'hashchange', handler: routeHandler },
{ target: window, event: 'popstate', handler: routeHandler },
{ target: window, event: 'model-resolver-locationchange', handler: routeHandler },
@@ -872,6 +872,10 @@ export const workflowStateMethods = {
const missingSignatureChanged = missingSignature
&& missingSignature !== this.activeMissingWorkflowSignature;
const graphStillLooksOld = routeChanged && previousSignature && signature === previousSignature;
if (this.activeTab === 'metadata' && !routeChanged) {
const promptsChanged = this.refreshMetadataPromptPreview?.(workflow);
if (promptsChanged && !signatureChanged) return;
}
const workflowChangeStillPending = (
String(reason || '').startsWith('node-')
&& !routeChanged
@@ -851,6 +851,7 @@ export const imageMetadataMethods = {
const imageState = state || getImageInspectorState(this);
const promptTransfer = this.getMetadataPromptTransferState(imageState);
const data = this.getMetadataPromptTransferData(imageState);
this._metadataPromptPreviewSignature = JSON.stringify(data.targetPrompts);
const sourceSummary = data.sourcePrompts.length
? `${data.sourcePrompts.length} editable prompt${data.sourcePrompts.length === 1 ? '' : 's'} found in the imported workflow.`
: imageState.loading
@@ -977,6 +978,12 @@ export const imageMetadataMethods = {
`;
},
refreshMetadataPromptPreview(workflow) {
const signature = JSON.stringify(extractWorkflowPrompts(workflow || {}).filter(prompt => !prompt.linked));
if (signature === this._metadataPromptPreviewSignature) return false;
return this.renderMetadataPromptTransferPanelInPlace({ fallback: false });
},
renderMetadataPromptTransferPanelInPlace({ fallback = true } = {}) {
const result = this.contentElement?.querySelector('[data-image-inspector-result]');
const currentPanel = result?.querySelector('.mr-image-prompt-transfer');