Add copy and resize controls to prompt previews
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user