Add copy and resize controls to prompt previews

This commit is contained in:
Dariusz L
2026-10-05 16:11:05 +02:00
parent 3829f95960
commit 67b7e4a454
3 changed files with 105 additions and 7 deletions
+46
View File
@@ -373,6 +373,10 @@ 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.equal((html.match(/data-image-prompt-copy/g) || []).length, 4);
assert.match(html, /class="mr-btn mr-btn-secondary mr-btn-sm mr-image-prompt-copy"[^>]*data-image-prompt-copy/);
assert.match(html, /data-tooltip="["]*Positive\/negative role detected from workflow connections\./);
assert.doesNotMatch(html, /class="mr-image-prompt-meta"/);
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"');
@@ -386,6 +390,48 @@ test('metadata prompt panel renders separate positive and negative transfer rows
assert.match(html, /Replace prompts/);
});
test('metadata prompt copy uses the visible prompt preview text', async () => {
const context = createContext(null, createRoleWorkflow());
const copied = [];
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
clipboard: {
async writeText(value) {
copied.push(value);
},
},
},
writable: true,
});
const button = {
innerHTML: '<svg></svg> Copy',
isConnected: false,
classList: {
add() {},
remove() {},
},
closest(selector) {
assert.equal(selector, '.mr-image-prompt-side');
return {
querySelector(query) {
assert.equal(query, '.mr-image-prompt-text');
return { textContent: 'visible prompt preview' };
},
};
},
};
try {
assert.equal(await context.copyMetadataPromptText(button), true);
assert.deepEqual(copied, ['visible prompt preview']);
} finally {
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator);
else delete globalThis.navigator;
}
});
test('metadata prompt selectors put the most likely prompt node first', () => {
const currentWorkflow = {
nodes: [
+17 -2
View File
@@ -5080,9 +5080,12 @@ button.mr-info-link:disabled {
}
.mr-image-prompt-select {
box-sizing: border-box;
width: 100%;
min-width: 0;
min-height: 26px;
margin: 0 !important;
align-self: stretch;
padding: var(--mr-spacing-4) var(--mr-spacing-7);
border: 1px solid var(--mr-border-strong, rgba(255, 255, 255, 0.24));
border-radius: var(--mr-radius-md, var(--mr-radius-lg));
@@ -5105,10 +5108,13 @@ button.mr-info-link:disabled {
.mr-image-prompt-text {
box-sizing: border-box;
width: 100%;
max-height: 150px;
height: auto;
min-height: 28px;
max-height: 70vh;
resize: vertical;
margin: 0;
overflow: auto;
padding: var(--mr-spacing-8);
padding: var(--mr-spacing-4) var(--mr-spacing-8);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--mr-radius-md, var(--mr-radius-lg));
background: rgba(0, 0, 0, 0.16);
@@ -5124,6 +5130,15 @@ button.mr-info-link:disabled {
background: rgba(255, 255, 255, 0.045);
}
.mr-image-prompt-copy {
align-self: flex-start;
}
.mr-image-prompt-copy.is-copied {
background: rgba(114, 211, 138, 0.16);
border-color: rgba(114, 211, 138, 0.28);
}
.mr-image-prompt-arrow {
color: var(--mr-accent);
font-size: var(--mr-font-lg);
@@ -1,4 +1,4 @@
import { safeStorage } from "../utils/html_utils.js";
import { copyTextWithFeedback, safeStorage } from "../utils/html_utils.js";
import {
isExistingResolvedModel,
toResolverContextModel,
@@ -17,6 +17,7 @@ const METADATA_TRANSFER_MODE_STORAGE_KEY = 'model_resolver_metadata_transfer_mod
const METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY = 'model_resolver_metadata_transfer_activity_scope';
const METADATA_PROMPT_ROLES = ['positive', 'negative'];
const MAX_PROMPT_PREVIEW_LENGTH = 12_000;
const COPY_ICON_HTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
const MERGE_LORA_NODE_TYPES = new Set([
'LoraLoaderV2',
@@ -264,6 +265,12 @@ export const imageMetadataMethods = {
return;
}
const promptCopyButton = event.target.closest?.('[data-image-prompt-copy]');
if (promptCopyButton) {
void this.copyMetadataPromptText(promptCopyButton);
return;
}
const action = event.target.closest?.('[data-image-inspector-action]')?.dataset?.imageInspectorAction;
if (!action) return;
if (action === 'choose-file') fileInput?.click();
@@ -726,6 +733,30 @@ export const imageMetadataMethods = {
return `${text.slice(0, MAX_PROMPT_PREVIEW_LENGTH)}\n…`;
},
getMetadataPromptCopyButton(prompt = {}) {
if (!String(prompt.text || '').trim()) return '';
return `
<button type="button" class="mr-btn mr-btn-secondary mr-btn-sm mr-image-prompt-copy" data-image-prompt-copy aria-label="Copy visible prompt text" data-tooltip="Copy visible prompt text">
${COPY_ICON_HTML} Copy
</button>
`;
},
async copyMetadataPromptText(button) {
const textElement = button?.closest?.('.mr-image-prompt-side')
?.querySelector?.('.mr-image-prompt-text');
const text = textElement?.textContent || '';
if (!text) return false;
await copyTextWithFeedback(String(text), button, {
successClass: 'is-copied',
successHtml: `${COPY_ICON_HTML} Copied`,
errorText: 'Failed',
duration: 1200,
});
return true;
},
getMetadataTooltipBadge(text, ariaLabel = 'More information') {
const tooltip = String(text ?? '').trim();
if (!tooltip) return '';
@@ -862,7 +893,7 @@ export const imageMetadataMethods = {
${sourceOptions}
</select>
<pre class="mr-image-prompt-text" title="${this.escapeHtml(String(row.source.text || ''))}">${this.escapeHtml(this.getMetadataPromptPreviewText(row.source) || '(empty)')}</pre>
<span class="mr-image-prompt-meta">${this.escapeHtml(sourceRoleHint)}</span>
${this.getMetadataPromptCopyButton(row.source)}
`
: '<p class="mr-image-prompt-empty">No imported prompt available.</p>';
const targetHtml = row.target
@@ -871,7 +902,7 @@ export const imageMetadataMethods = {
${targetOptions}
</select>
<pre class="mr-image-prompt-text is-current" title="${this.escapeHtml(String(row.target.text || ''))}">${this.escapeHtml(this.getMetadataPromptPreviewText(row.target) || '(empty)')}</pre>
<span class="mr-image-prompt-meta">${this.escapeHtml(targetRoleHint)}</span>
${this.getMetadataPromptCopyButton(row.target)}
`
: '<p class="mr-image-prompt-empty">No current text field available.</p>';
return `
@@ -885,12 +916,18 @@ export const imageMetadataMethods = {
</div>
<div class="mr-image-prompt-grid">
<div class="mr-image-prompt-side${targetContextMenuAttrs ? ' is-context-menu' : ''}"${targetContextMenuAttrs}>
<span class="mr-image-prompt-side-label">Current text field</span>
<span class="mr-image-prompt-side-label">
Current text field
${this.getMetadataTooltipBadge(targetRoleHint, `Current ${row.role} prompt role information`)}
</span>
${targetHtml}
</div>
<span class="mr-image-prompt-arrow" aria-hidden="true">→</span>
<div class="mr-image-prompt-side">
<span class="mr-image-prompt-side-label">Imported</span>
<span class="mr-image-prompt-side-label">
Imported
${this.getMetadataTooltipBadge(sourceRoleHint, `Imported ${row.role} prompt role information`)}
</span>
${sourceHtml}
</div>
</div>