diff --git a/.agents/diagnostics/material_library_core_logic.md b/.agents/diagnostics/material_library_core_logic.md index a65f089..9bf9a0e 100644 --- a/.agents/diagnostics/material_library_core_logic.md +++ b/.agents/diagnostics/material_library_core_logic.md @@ -111,6 +111,7 @@ user//workflows/anomalous_materials/ 11. 删除素材时同时删除其私有图片资产,不影响原始输出图。 12. 图库详情的每个节点卡直接提供“存入素材库”;勾选多个节点后,节点列表顶部提供“存入素材库 (N)”。底部只保存完整图片与工作流,不再用范围下拉框混合两类操作。节点参数素材不提供误导性的“打开完整工作流”。 13. 详情缓存最多保留 16 张的解析结果;正常情况下直接使用后端已验证的 workflow,不在浏览器中再次读取整张 PNG。旧后端兜底读取可随导航中止,且不缓存重复的 workflow/节点完整值;本地模型缩略图匹配只在打开“模型资产”页时运行。 +14. 素材库卡片可原地展开查看模型引用和其实际可用的节点参数;完整素材展示全部节点,节点选择素材只展示已保存的节点。闭合卡片时释放完整响应和详细 DOM。 ## 七、明确未纳入本次 MVP 的内容 diff --git a/CHANGELOG.md b/CHANGELOG.md index a589b33..48f6508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - **Optional save-time model verification (保存时可选模型核验)**: Recipe saving now performs a lightweight cached-identity check. Recognized model references missing identity are listed in the save dialog, while full SHA-256 calculation remains explicitly opt-in. This is especially useful for hash-only Model Doctor categories such as VAE and text encoders. - **Model lifecycle consistency (模型生命周期一致性)**: Partial append remaps and merges node-scoped model Hash records, while applying a local model match updates the workflow value, structured model reference, base-model summary, and Hash index together. - **Recipe-scoped model notes (配方模型备注)**: Each model reference can keep a bounded personal note. Package export asks whether to include these notes, and version comparison reports note and identity changes. -- **Material Library MVP (统一素材库 MVP)**: Main Gallery and Recipe gallery images now expose a non-intrusive two-column parameter detail view and can be saved as one curated bundle containing the original image, exact embedded workflow, model provenance, and reusable node blocks. Core metrics remain concise, while a lazy two-level inspector exposes every serialized node widget value, node properties, mode, and volatile-parameter marker on demand. Every node card now has a direct “Save to Library” action; checked nodes share a colocated multi-save action above the list, while the footer is reserved for the full image/workflow snapshot. Focused node materials feed Node Assistant without exposing a misleading full-workflow action. Detail navigation bounds parsed metadata to a 16-entry LRU, cancels obsolete reads, avoids a second full-PNG browser allocation, removes duplicated widget payloads, and defers local-model preview resolution until the Models tab opens. “Copy Share Text” clearly exports Civitai/WebUI-style plain text without changing the canvas. Main Gallery keeps its existing click, drag, delete, and cover-selection behavior. The Workspace Material Library opens complete snapshots in a new canvas, while Node Assistant filters matching blocks by the selected node type, skips volatile seeds, and hands remapped Hash evidence to Model Doctor. +- **Material Library MVP (统一素材库 MVP)**: Main Gallery and Recipe gallery images now expose a non-intrusive two-column parameter detail view and can be saved as one curated bundle containing the original image, exact embedded workflow, model provenance, and reusable node blocks. Core metrics remain concise, while a lazy two-level inspector exposes every serialized node widget value, node properties, mode, and volatile-parameter marker on demand. Every node card now has a direct “Save to Library” action; checked nodes share a colocated multi-save action above the list, while the footer is reserved for the full image/workflow snapshot. Focused node materials feed Node Assistant without exposing a misleading full-workflow action. Saved Material cards can expand in place to inspect their scoped model references and exact node parameters; collapsing releases the full response and detailed DOM. Detail navigation bounds parsed metadata to a 16-entry LRU, cancels obsolete reads, avoids a second full-PNG browser allocation, removes duplicated widget payloads, and defers local-model preview resolution until the Models tab opens. “Copy Share Text” clearly exports Civitai/WebUI-style plain text without changing the canvas. Main Gallery keeps its existing click, drag, delete, and cover-selection behavior. The Workspace Material Library opens complete snapshots in a new canvas, while Node Assistant filters matching blocks by the selected node type, skips volatile seeds, and hands remapped Hash evidence to Model Doctor. ### ✨ Interface & Access - **Native Configurable Shortcut (原生可配置快捷键)**: Added `Ctrl + Shift + M` as the default shortcut for opening the browser. The plugin's Interface settings show the currently active binding and open ComfyUI's native key recorder for customization. Conflict detection, overwrite confirmation, removal, reserved-key guidance, and modal guards therefore remain centralized without a separate global keyboard listener. diff --git a/docs/architecture/material-library.md b/docs/architecture/material-library.md index 5d88c95..cb8c3a0 100644 --- a/docs/architecture/material-library.md +++ b/docs/architecture/material-library.md @@ -47,6 +47,12 @@ material action appears only on hover and is hidden during cover selection. `ui_doctor.js` owns application to the currently selected node because that mutation already belongs to Node Assistant. +Material cards remain summary-only until their parameter disclosure is opened. +Opening one card fetches `material_full` and shows its scoped model references +and node blocks; exact widget values remain nested under each node. Collapsing +the card aborts an unfinished request and releases the expanded payload/DOM so +closed libraries do not accumulate full workflows in browser memory. + A full material workflow is exact and retains its seed. Node-sized reuse is a preset operation and therefore uses the existing transactional parameter application path, which skips known volatile seed widgets. The lookup endpoint diff --git a/web/modules/locales.js b/web/modules/locales.js index 5d6befa..2fa02fc 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -77,6 +77,8 @@ export const i18n = { materialDeleteError: '素材删除失败。', materialViewDetails: '查看详情', materialViewParameters: '查看参数', + materialDetailLoadError: '无法读取这个素材的详细参数。', + materialNoNodeParameters: '这个素材没有可展示的节点参数。', materialImageDetail: '图片参数详情', workbenchFullscreen: '全屏工作台', workbenchWindowed: '窗口模式', @@ -943,6 +945,8 @@ export const i18n = { materialDeleteError: 'Could not delete the material.', materialViewDetails: 'View Details', materialViewParameters: 'View Parameters', + materialDetailLoadError: 'Could not load this material\'s detailed parameters.', + materialNoNodeParameters: 'This material has no node parameters to display.', materialImageDetail: 'Image Parameter Details', workbenchFullscreen: 'Fullscreen Workbench', workbenchWindowed: 'Windowed Mode', diff --git a/web/modules/ui_materials.js b/web/modules/ui_materials.js index d021e8d..b2f8e8d 100644 --- a/web/modules/ui_materials.js +++ b/web/modules/ui_materials.js @@ -100,13 +100,43 @@ function hideSiblingWorkspaceViews(owner) { owner.notebookMaterialsTab?.classList.add('active'); } -async function fetchMaterial(filename) { - const response = await fetch(`/anomalous/material_full?filename=${encodeURIComponent(filename)}`, { cache: 'no-store' }); +async function fetchMaterial(filename, options = {}) { + const response = await fetch(`/anomalous/material_full?filename=${encodeURIComponent(filename)}`, { + cache: 'no-store', + signal: options.signal, + }); const payload = await jsonResponse(response, 'material load failed'); if (payload.status !== 'success') throw new Error(payload.message || 'material load failed'); return payload; } +function renderExpandedMaterial(content, payload) { + const references = Array.isArray(payload.data?.model_references) ? payload.data.model_references : []; + if (references.length) { + sectionLabel(content, t('materialModelSummary', { count: references.length })); + const models = document.createElement('div'); + models.className = 'anomalous-material-expanded-models'; + for (const reference of references) { + const item = document.createElement('div'); + item.className = 'anomalous-material-expanded-model'; + const rawValue = reference.saved_value || reference.name || t('materialUntitled'); + const name = text(item, 'span', fileBaseName(rawValue)); + name.title = String(rawValue); + text(item, 'small', reference.category || 'model'); + models.appendChild(item); + } + content.appendChild(models); + } + + const blocks = Array.isArray(payload.node_blocks) ? payload.node_blocks : []; + sectionLabel(content, t('materialDetailedNodeParameters', { count: blocks.length })); + if (blocks.length) { + renderDetailedNodeCards(content, blocks); + } else { + text(content, 'p', t('materialNoNodeParameters'), 'anomalous-material-muted'); + } +} + async function openMaterialWorkflow(owner, filename) { const payload = await fetchMaterial(filename); if (!payload.data?.workflow || typeof app.loadGraphData !== 'function') throw new Error('material workflow unavailable'); @@ -181,6 +211,45 @@ function renderMaterialCard(owner, material) { await owner.refreshMaterials?.(); }; card.appendChild(actions); + + const details = document.createElement('details'); + details.className = 'anomalous-material-card-details'; + const summary = document.createElement('summary'); + summary.textContent = `⚙️ ${t('materialViewParameters')}`; + details.appendChild(summary); + const detailContent = document.createElement('div'); + detailContent.className = 'anomalous-material-card-detail-content'; + details.appendChild(detailContent); + + let requestController = null; + details.addEventListener('toggle', async () => { + card.classList.toggle('is-expanded', details.open); + if (!details.open) { + requestController?.abort(); + requestController = null; + detailContent.replaceChildren(); + return; + } + + const controller = new AbortController(); + requestController = controller; + detailContent.replaceChildren(); + text(detailContent, 'p', t('loading'), 'anomalous-material-muted'); + try { + const payload = await fetchMaterial(material.filename, { signal: controller.signal }); + if (!details.open) return; + detailContent.replaceChildren(); + renderExpandedMaterial(detailContent, payload); + } catch (error) { + if (error?.name === 'AbortError') return; + console.error('Could not expand material parameters:', error); + detailContent.replaceChildren(); + text(detailContent, 'p', t('materialDetailLoadError'), 'anomalous-material-muted'); + } finally { + if (requestController === controller) requestController = null; + } + }); + card.appendChild(details); return card; } diff --git a/web/styles.css b/web/styles.css index 41dcf99..31bf514 100644 --- a/web/styles.css +++ b/web/styles.css @@ -3651,6 +3651,71 @@ dialog.anomalous-gallery-viewer[open] { .anomalous-material-card-actions { display: flex; gap: 8px; margin-top: auto; padding: 0 14px 14px; } .anomalous-material-card-actions .anomalous-btn-primary { flex: 1; } +.anomalous-material-card.is-expanded { + grid-column: 1 / -1; +} + +.anomalous-material-card-details { + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.anomalous-material-card-details > summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 11px 14px; + color: #aeb8c8; + font-size: 0.76rem; + cursor: pointer; + list-style: none; + user-select: none; +} + +.anomalous-material-card-details > summary::-webkit-details-marker { display: none; } +.anomalous-material-card-details > summary:hover { background: rgba(255, 255, 255, 0.035); color: #f2f5f9; } +.anomalous-material-card-details > summary::after { content: '⌄'; color: #738096; transition: transform 0.15s ease; } +.anomalous-material-card-details[open] > summary::after { transform: rotate(180deg); } + +.anomalous-material-card-detail-content { + max-height: min(58vh, 620px); + overflow: auto; + padding: 4px 14px 16px; +} + +.anomalous-material-expanded-models { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 7px; + margin: 8px 0 14px; +} + +.anomalous-material-expanded-model { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 7px 9px; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 7px; + background: rgba(0, 0, 0, 0.16); +} + +.anomalous-material-expanded-model span { + overflow: hidden; + color: #cbd4df; + font-size: 0.7rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.anomalous-material-expanded-model small { + flex: 0 0 auto; + color: #6fcbb7; + font-size: 0.62rem; +} + /* Professional Studio Material Detail Modal */ .anomalous-material-detail-overlay { position: fixed;