From 7705cfc6881b2fd2ac3d9c4d919a14c19eb796ef Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Fri, 16 Oct 2026 18:26:07 +0200 Subject: [PATCH] Make metadata inspector sections collapsible and normalize spacing --- tests/test_metadata_collapse.mjs | 198 ++++++++++++++++++ web/css/resolver-main.css | 175 +++++++++++++++- .../views/tabs_image_metadata_methods.js | 176 ++++++++++++++-- web/resolver/views/tabs_loaded_methods.js | 30 ++- web/utils/icon_utils.js | 21 +- 5 files changed, 571 insertions(+), 29 deletions(-) create mode 100644 tests/test_metadata_collapse.mjs diff --git a/tests/test_metadata_collapse.mjs b/tests/test_metadata_collapse.mjs new file mode 100644 index 0000000..8af3284 --- /dev/null +++ b/tests/test_metadata_collapse.mjs @@ -0,0 +1,198 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { imageMetadataMethods } from '../web/resolver/views/tabs_image_metadata_methods.js'; +import { tabsLoadedMethods } from '../web/resolver/views/tabs_loaded_methods.js'; + +globalThis.window = globalThis; + +function createMockContext() { + const memory = new Map(); + const mockStorage = { + getItem: (k) => (memory.has(k) ? memory.get(k) : null), + setItem: (k, v) => memory.set(k, String(v)), + removeItem: (k) => memory.delete(k), + }; + return { + ...imageMetadataMethods, + ...tabsLoadedMethods, + escapeHtml: (s) => String(s ?? ''), + escapeJsString: (s) => JSON.stringify(s), + getMetadataTooltipBadge: (help, label) => `?`, + getCategoryDisplayName: (cat) => cat, + getModelToken: (model) => model.name || '', + getContextMenuAttrs: () => '', + stripModelExtension: (name) => String(name || '').replace(/\.[^/.]+$/, ''), + getMissingModelKey: (model) => model.name || 'key', + getModelPreviewTooltipAttrs: () => '', + bindLoadedModelSelection: () => {}, + renderStatusMessage: (msg, type = 'info') => `
${msg}
`, + storage: mockStorage, + }; +} + +test('image metadata sections default to expanded', () => { + const ctx = createMockContext(); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('tools'), false); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('summary'), false); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('models'), false); +}); + +test('toggling section marks it collapsed and persists state', () => { + const ctx = createMockContext(); + const state = {}; + ctx.toggleImageInspectorSection('prompt-transfer'); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), true); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('model-transfer'), false); + + ctx.toggleImageInspectorSection('prompt-transfer'); + assert.strictEqual(ctx.isImageInspectorSectionCollapsed('prompt-transfer'), false); +}); + +test('renderMetadataPromptTransferPanel renders collapse button with chevron and aria-expanded', () => { + const ctx = createMockContext(); + const state = { + promptTransfer: { + loading: false, + applying: false, + error: '', + pairs: {}, + prompts: { source: [], target: [] }, + }, + }; + const expandedHtml = ctx.renderMetadataPromptTransferPanel(state); + assert.ok(expandedHtml.includes('data-image-inspector-collapse="prompt-transfer"')); + assert.ok(expandedHtml.includes('aria-expanded="true"')); + assert.ok(!expandedHtml.includes('mr-image-prompt-transfer is-collapsed')); + + ctx.toggleImageInspectorSection('prompt-transfer'); + const collapsedHtml = ctx.renderMetadataPromptTransferPanel(state); + assert.ok(collapsedHtml.includes('aria-expanded="false"')); + assert.ok(collapsedHtml.includes('mr-image-prompt-transfer is-collapsed')); +}); + +test('renderMetadataTransferPanel renders collapse button with chevron and aria-expanded', () => { + const ctx = createMockContext(); + const state = { + transfer: { + loading: false, + refreshing: false, + applying: false, + undoing: false, + error: '', + targetGroups: [], + selectedNodeKeys: new Set(), + }, + }; + const expandedHtml = ctx.renderMetadataTransferPanel(state); + assert.ok(expandedHtml.includes('data-image-inspector-collapse="model-transfer"')); + assert.ok(expandedHtml.includes('aria-expanded="true"')); + assert.ok(!expandedHtml.includes('mr-image-transfer is-collapsed')); + + ctx.toggleImageInspectorSection('model-transfer'); + const collapsedHtml = ctx.renderMetadataTransferPanel(state); + assert.ok(collapsedHtml.includes('aria-expanded="false"')); + assert.ok(collapsedHtml.includes('mr-image-transfer is-collapsed')); +}); + +test('displayLoadedModels renders collapse button and tooltip badge, hiding subtitle when collapsible', () => { + const ctx = createMockContext(); + const container = { innerHTML: '', dataset: {} }; + const data = { + total: 1, + loaded_models: [ + { name: 'model.safetensors', category: 'checkpoints', active: true } + ], + }; + + // Non-collapsible (Loaded Models tab) - shows subtitle, no tooltip badge + ctx.displayLoadedModels(container, data, { collapsible: false }); + assert.ok(!container.innerHTML.includes('data-image-inspector-collapse')); + assert.ok(container.innerHTML.includes('

Loaded Models')); + assert.ok(container.innerHTML.includes('class="mr-loaded-models-subtitle"')); + + // Collapsible (Metadata tab) - shows tooltip badge, hides subtitle paragraph + ctx.displayLoadedModels(container, data, { + collapsible: true, + isCollapsed: false, + sectionId: 'models', + title: 'Referenced Models', + tooltip: 'Models reported by embedded Civitai metadata.', + }); + assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"')); + assert.ok(container.innerHTML.includes('aria-expanded="true"')); + assert.ok(container.innerHTML.includes('Referenced Models')); + assert.ok(container.innerHTML.includes('class="mr-tooltip-badge"')); + assert.ok(container.innerHTML.includes('Models reported by embedded Civitai metadata.')); + assert.ok(!container.innerHTML.includes('class="mr-loaded-models-subtitle"')); + + // Collapsible and collapsed + ctx.displayLoadedModels(container, data, { + collapsible: true, + isCollapsed: true, + sectionId: 'models', + title: 'Referenced Models', + tooltip: 'Models reported by embedded Civitai metadata.', + }); + assert.ok(container.innerHTML.includes('data-image-inspector-collapse="models"')); + assert.ok(container.innerHTML.includes('aria-expanded="false"')); +}); + +test('renderImageMetadataResult renders Extracted Metadata with tooltip badge and no subtitle paragraph', () => { + const ctx = createMockContext(); + const resultEl = { + innerHTML: '', + querySelector: () => ({ innerHTML: '', dataset: {} }), + }; + ctx.contentElement = { + querySelector: (sel) => { + if (sel === '[data-image-inspector-result]') { + return resultEl; + } + return null; + }, + }; + ctx.imageInspectorState = { + metadata: { + filename: 'test.png', + image: { width: 512, height: 512, format: 'PNG' }, + }, + loadedModels: null, + loading: false, + error: '', + }; + ctx.renderMetadataPromptTransferPanel = () => ''; + ctx.renderMetadataTransferPanel = () => ''; + + ctx.renderImageMetadataResult(); + assert.ok(resultEl.innerHTML.includes('Extracted Metadata')); + assert.ok(resultEl.innerHTML.includes('data-image-inspector-collapse="summary"')); + assert.ok(resultEl.innerHTML.includes('Read-only information from the selected source.')); + assert.ok(resultEl.innerHTML.includes('class="mr-tooltip-badge"')); + assert.ok(!resultEl.innerHTML.includes('class="mr-loaded-models-subtitle"')); +}); + +test('renderImageMetadataResult without metadata renders prompt transfer panel first', () => { + const ctx = createMockContext(); + const resultEl = { + innerHTML: '', + querySelector: () => null, + }; + ctx.contentElement = { + querySelector: (sel) => (sel === '[data-image-inspector-result]' ? resultEl : null), + }; + ctx.imageInspectorState = { + metadata: null, + loadedModels: null, + loading: false, + error: '', + }; + ctx.renderMetadataPromptTransferPanel = () => ''; + ctx.renderMetadataTransferPanel = () => ''; + + ctx.renderImageMetadataResult(); + assert.ok(resultEl.innerHTML.startsWith('
:first-child, +.mr-image-inspector-result > :first-child, +#model-resolver-modal .mr-image-inspector-result > .mr-image-prompt-transfer:first-child, +.mr-image-inspector-result > .mr-image-prompt-transfer:first-child, +#model-resolver-modal .mr-image-inspector-result > .mr-image-transfer:first-child, +.mr-image-inspector-result > .mr-image-transfer:first-child { + margin-top: 0; +} + .mr-image-inspector-summary { padding-top: var(--mr-spacing-12); border-top: 1px solid var(--mr-border); @@ -4944,9 +5021,7 @@ button.mr-info-link:disabled { #model-resolver-modal .mr-image-inspector-summary .mr-loaded-models-header, .mr-image-inspector-summary .mr-loaded-models-header { - padding-left: 0; - padding-right: 0; - padding-top: 0; + padding: 0; border-bottom: 0; margin-bottom: var(--mr-spacing-8); } @@ -4987,9 +5062,9 @@ button.mr-info-link:disabled { #model-resolver-modal .mr-image-inspector-models .mr-loaded-models-header, .mr-image-inspector-models .mr-loaded-models-header { - padding-left: 0; - padding-right: 0; - padding-top: 0; + padding: 0; + border-bottom: 0; + align-items: flex-start; } #model-resolver-modal .mr-image-inspector-models .mr-models-list, @@ -5064,6 +5139,94 @@ button.mr-info-link:disabled { background: linear-gradient(90deg, rgba(114, 211, 138, 0.28), var(--mr-border) 34%, transparent 100%); } +#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools, +.mr-image-inspector.is-tools-collapsed .mr-image-inspector-tools { + display: none; +} + +#model-resolver-modal .mr-image-inspector.is-tools-collapsed .mr-image-inspector-header, +.mr-image-inspector.is-tools-collapsed .mr-image-inspector-header { + padding-bottom: var(--mr-spacing-12); +} + +#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table, +.mr-image-inspector-summary.is-collapsed .mr-image-inspector-info-table { + display: none; +} + +#model-resolver-modal .mr-image-inspector-summary.is-collapsed .mr-loaded-models-header, +.mr-image-inspector-summary.is-collapsed .mr-loaded-models-header { + margin-bottom: 0; + padding: 0; +} + +#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list, +.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-list, +#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions, +.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-actions, +#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-error-text, +.mr-image-prompt-transfer.is-collapsed .mr-error-text { + display: none; +} + +#model-resolver-modal .mr-image-prompt-transfer.is-collapsed, +.mr-image-prompt-transfer.is-collapsed { + margin-bottom: var(--mr-spacing-12); +} + +#model-resolver-modal .mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header, +.mr-image-prompt-transfer.is-collapsed .mr-image-prompt-header { + margin-bottom: 0; +} + +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-mode, +.mr-image-transfer.is-collapsed .mr-image-transfer-mode, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-activity, +.mr-image-transfer.is-collapsed .mr-image-transfer-activity, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-category-list, +.mr-image-transfer.is-collapsed .mr-image-transfer-category-list, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-actions, +.mr-image-transfer.is-collapsed .mr-image-transfer-actions, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-loading, +.mr-image-transfer.is-collapsed .mr-image-transfer-loading, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-note, +.mr-image-transfer.is-collapsed .mr-image-transfer-note, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-inspector-loading, +.mr-image-transfer.is-collapsed .mr-image-inspector-loading, +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-error-text, +.mr-image-transfer.is-collapsed .mr-error-text { + display: none; +} + +#model-resolver-modal .mr-image-transfer.is-collapsed, +.mr-image-transfer.is-collapsed { + margin-bottom: var(--mr-spacing-12); +} + +#model-resolver-modal .mr-image-transfer.is-collapsed .mr-image-transfer-header, +.mr-image-transfer.is-collapsed .mr-image-transfer-header { + margin-bottom: 0; +} + +#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list, +.mr-image-inspector-models.is-collapsed .mr-models-list, +#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-models-list-pad, +.mr-image-inspector-models.is-collapsed .mr-models-list-pad, +#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-copy-section, +.mr-image-inspector-models.is-collapsed .mr-copy-section, +#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-filter-row, +.mr-image-inspector-models.is-collapsed .mr-loaded-filter-row { + display: none; +} + +#model-resolver-modal .mr-image-inspector-models.is-collapsed .mr-loaded-models-header, +.mr-image-inspector-models.is-collapsed .mr-loaded-models-header { + margin-bottom: 0; + padding: 0; + border-bottom: 0; + align-items: flex-start; +} + .mr-image-prompt-header, .mr-image-prompt-actions { display: flex; diff --git a/web/resolver/views/tabs_image_metadata_methods.js b/web/resolver/views/tabs_image_metadata_methods.js index aaeef09..3372533 100644 --- a/web/resolver/views/tabs_image_metadata_methods.js +++ b/web/resolver/views/tabs_image_metadata_methods.js @@ -1,4 +1,5 @@ import { copyTextWithFeedback, safeStorage } from "../utils/html_utils.js"; +import { getSvgIcon } from "../../utils/icon_utils.js"; import { isExistingResolvedModel, toResolverContextModel, @@ -15,6 +16,7 @@ const MAX_IMAGE_FILE_SIZE = 64 * 1024 * 1024; const MAX_WORKFLOW_FILE_SIZE = 16 * 1024 * 1024; const METADATA_TRANSFER_MODE_STORAGE_KEY = 'model_resolver_metadata_transfer_mode'; const METADATA_TRANSFER_ACTIVITY_SCOPE_STORAGE_KEY = 'model_resolver_metadata_transfer_activity_scope'; +const METADATA_COLLAPSED_SECTIONS_STORAGE_KEY = 'model_resolver_metadata_collapsed_sections'; const METADATA_PROMPT_ROLES = ['positive', 'negative']; const MAX_PROMPT_PREVIEW_LENGTH = 12_000; const COPY_ICON_HTML = ''; @@ -84,6 +86,7 @@ function getImageInspectorState(dialog) { transfer: null, promptTransfer: null, lastTransfer: null, + collapsedSections: null, }; } return dialog.imageInspectorState; @@ -184,6 +187,84 @@ export const imageMetadataMethods = { return Boolean(workflowSignature) && transfer.workflowSignature === workflowSignature; }, + getImageInspectorCollapsedSections(state = null) { + const rootState = getImageInspectorState(this); + const imageState = state || rootState; + if (rootState && rootState.collapsedSections instanceof Set) { + imageState.collapsedSections = rootState.collapsedSections; + return rootState.collapsedSections; + } + if (!(imageState.collapsedSections instanceof Set)) { + let initial = []; + const stored = safeStorage.getItem(METADATA_COLLAPSED_SECTIONS_STORAGE_KEY); + if (stored) { + try { + initial = JSON.parse(stored); + } catch { + initial = []; + } + } + const set = new Set(Array.isArray(initial) ? initial : []); + imageState.collapsedSections = set; + if (rootState) rootState.collapsedSections = set; + } + return imageState.collapsedSections; + }, + + isImageInspectorSectionCollapsed(sectionId, state = null) { + if (!sectionId) return false; + const collapsed = this.getImageInspectorCollapsedSections(state); + return collapsed instanceof Set ? collapsed.has(sectionId) : false; + }, + + toggleImageInspectorSection(sectionId) { + if (!sectionId) return; + const state = getImageInspectorState(this); + const collapsed = this.getImageInspectorCollapsedSections(state); + const nextCollapsed = !collapsed.has(sectionId); + if (nextCollapsed) { + collapsed.add(sectionId); + } else { + collapsed.delete(sectionId); + } + try { + safeStorage.setItem( + METADATA_COLLAPSED_SECTIONS_STORAGE_KEY, + JSON.stringify(Array.from(collapsed)), + ); + } catch { + // ignore storage errors + } + this.applyImageInspectorSectionCollapsed(sectionId, nextCollapsed); + }, + + applyImageInspectorSectionCollapsed(sectionId, isCollapsed) { + const root = this.contentElement?.querySelector('.mr-image-inspector'); + if (!root) return; + + let container = null; + if (sectionId === 'tools') { + root.classList.toggle('is-tools-collapsed', isCollapsed); + } else if (sectionId === 'summary') { + container = root.querySelector('.mr-image-inspector-summary'); + } else if (sectionId === 'prompt-transfer') { + container = root.querySelector('.mr-image-prompt-transfer'); + } else if (sectionId === 'model-transfer') { + container = root.querySelector('.mr-image-transfer'); + } else if (sectionId === 'models') { + container = root.querySelector('.mr-image-inspector-models'); + } + + if (container) { + container.classList.toggle('is-collapsed', isCollapsed); + } + + const btn = root.querySelector(`[data-image-inspector-collapse="${sectionId}"]`); + if (btn) { + btn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true'); + } + }, + loadImageMetadata() { if (!this.contentElement) return null; this.contentElement.style.overflowY = 'auto'; @@ -198,13 +279,20 @@ export const imageMetadataMethods = { renderImageMetadataShell() { if (!this.contentElement) return; + const isToolsCollapsed = this.isImageInspectorSectionCollapsed('tools'); const existingRoot = this.contentElement.querySelector('.mr-image-inspector'); if (!existingRoot) { this.contentElement.innerHTML = ` -
+
-

Image Metadata ${this.getMetadataTooltipBadge('Drop an image or workflow JSON here, or paste JSON into the box below.', 'Image metadata help')}

+
+ + ${this.getMetadataTooltipBadge('Drop an image or workflow JSON here, or paste JSON into the box below.', 'Image metadata help')} +
@@ -222,6 +310,10 @@ export const imageMetadataMethods = {
`; this.bindImageMetadataEvents(); + } else { + existingRoot.classList.toggle('is-tools-collapsed', isToolsCollapsed); + const btn = existingRoot.querySelector('[data-image-inspector-collapse="tools"]'); + if (btn) btn.setAttribute('aria-expanded', isToolsCollapsed ? 'false' : 'true'); } this.bindTooltips?.(this.contentElement); @@ -251,6 +343,14 @@ export const imageMetadataMethods = { let dragDepth = 0; root.addEventListener('click', (event) => { + const collapseToggle = event.target.closest?.('[data-image-inspector-collapse]'); + if (collapseToggle) { + event.preventDefault(); + event.stopPropagation(); + this.toggleImageInspectorSection(collapseToggle.dataset.imageInspectorCollapse); + return; + } + const categoryToggle = event.target.closest?.('[data-image-transfer-category-toggle]'); if (categoryToggle) { this.toggleMetadataTransferCategory(categoryToggle.dataset.imageTransferCategoryToggle); @@ -632,20 +732,27 @@ export const imageMetadataMethods = { ` : ''; const modelContainerId = `mr-image-inspector-models-${Date.now()}`; + const isSummaryCollapsed = this.isImageInspectorSectionCollapsed('summary', state); + const isModelsCollapsed = this.isImageInspectorSectionCollapsed('models', state); result.innerHTML = ` -
+
-

Extracted Metadata

-

Read-only information from the selected source.

+
+ + ${this.getMetadataTooltipBadge('Read-only information from the selected source.', 'Extracted metadata help')} +
${rowsHtml}${parametersHtml}
${this.renderMetadataPromptTransferPanel(state)} ${this.renderMetadataTransferPanel(state)} -
+
`; @@ -654,8 +761,11 @@ export const imageMetadataMethods = { if (!metadata.workflow && civitaiModelData.total) { this.displayLoadedModels(modelContainer, civitaiModelData, { title: 'Referenced Models', - subtitle: 'Models reported by embedded Civitai metadata.', + tooltip: 'Models reported by embedded Civitai metadata.', includeContextMenu: false, + collapsible: true, + isCollapsed: isModelsCollapsed, + sectionId: 'models', }); } else if (!metadata.workflow) { modelContainer.innerHTML = this.renderStatusMessage( @@ -663,7 +773,13 @@ export const imageMetadataMethods = { 'info' ); } else if (state.loadedModels) { - this.displayLoadedModels(modelContainer, state.loadedModels); + this.displayLoadedModels(modelContainer, state.loadedModels, { + title: 'Referenced Models', + tooltip: 'Models detected in the imported ComfyUI workflow.', + collapsible: true, + isCollapsed: isModelsCollapsed, + sectionId: 'models', + }); } else { modelContainer.innerHTML = this.renderStatusMessage( 'No loaded model scan is available yet.', @@ -953,12 +1069,17 @@ export const imageMetadataMethods = { const statusHtml = promptTransfer.error ? `

${this.escapeHtml(promptTransfer.error)}

` : ''; + const isPromptCollapsed = this.isImageInspectorSectionCollapsed?.('prompt-transfer', imageState) || false; return ` -
+
-
-

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

+
+ + ${this.getMetadataTooltipBadge(promptHelpText, 'Prompt transfer help')}
${statusHtml} @@ -1717,9 +1838,19 @@ export const imageMetadataMethods = { ? 'The imported image has no embedded ComfyUI workflow.' : 'Import a workflow with model metadata to enable Apply transfer.'; + const isTransferCollapsed = this.isImageInspectorSectionCollapsed?.('model-transfer', state) || false; + if (transfer.loading) { return ` -
+
+
+
+ +
+
Scanning the current workflow for target nodes...

${this.escapeHtml(sourceSummary)}

@@ -1727,17 +1858,20 @@ export const imageMetadataMethods = { } if (transfer.error) { return ` -
+
-
-

Transfer models to current workflow

-

${this.escapeHtml(transfer.error)}

+
+
${undoButton}
+

${this.escapeHtml(transfer.error)}

${this.escapeHtml(sourceSummary)}

`; @@ -1907,10 +2041,14 @@ export const imageMetadataMethods = { : '', ].filter(Boolean).join('\n\n'); return ` -
+
-
-

Transfer models to current workflow ${this.getMetadataTooltipBadge(transferHelpText, 'Model transfer help')}

+
+ + ${this.getMetadataTooltipBadge(transferHelpText, 'Model transfer help')}
${undoButton} diff --git a/web/resolver/views/tabs_loaded_methods.js b/web/resolver/views/tabs_loaded_methods.js index 1b58a70..4552879 100644 --- a/web/resolver/views/tabs_loaded_methods.js +++ b/web/resolver/views/tabs_loaded_methods.js @@ -397,11 +397,36 @@ export const tabsLoadedMethods = { : ''; const modelSubtitle = subtitle || `${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}`; + const isCollapsed = Boolean(options.isCollapsed); + const tooltipText = options.tooltip || (options.collapsible && subtitle ? subtitle : ''); + const tooltipHtml = tooltipText + ? (typeof this.getMetadataTooltipBadge === 'function' + ? this.getMetadataTooltipBadge(tooltipText, `${title} help`) + : `?`) + : ''; + + const titleHtml = options.collapsible + ? ` +
+ + ${tooltipHtml} +
+ ` + : `

${this.escapeHtml(title)} ${total}${tooltipHtml}

`; + + const showSubtitle = (options.showSubtitle ?? !options.collapsible) && Boolean(modelSubtitle); + const subtitleHtml = showSubtitle + ? `

${this.escapeHtml(modelSubtitle)}

` + : ''; + let html = `
-

${this.escapeHtml(title)} ${total}

-

${this.escapeHtml(modelSubtitle)}

+ ${titleHtml} + ${subtitleHtml}
@@ -504,6 +529,7 @@ export const tabsLoadedMethods = { if (typeof container?.querySelector === 'function') { this.bindLoadedModelSelection?.(container); + this.bindTooltips?.(container); } }, diff --git a/web/utils/icon_utils.js b/web/utils/icon_utils.js index e89cee4..1419c79 100644 --- a/web/utils/icon_utils.js +++ b/web/utils/icon_utils.js @@ -1,5 +1,19 @@ // icon_utils - Utility for loading SVG icons -import { app } from "../../../scripts/app.js"; +let appInstance = null; +async function getApp() { + if (appInstance) return appInstance; + if (typeof window !== "undefined" && window.app) { + appInstance = window.app; + return appInstance; + } + try { + const mod = await import("../../../scripts/app.js"); + appInstance = mod?.app || null; + } catch { + // Not in ComfyUI environment + } + return appInstance; +} const SVG_ICONS = { trash2: ``, @@ -100,7 +114,10 @@ export function loadIcons(icons = {}, iconColor = "#dddddd", userColors = {}) { const svg = SVG_ICONS[name].replaceAll("{color}", color); const img = new Image(); - img.onload = () => app.graph.setDirtyCanvas(true); + img.onload = async () => { + const app = await getApp(); + app?.graph?.setDirtyCanvas(true); + }; img.src = `data:image/svg+xml;base64,${btoa(svg)}`; icons[name] = img;