feat(materials): expand saved parameter details
This commit is contained in:
@@ -111,6 +111,7 @@ user/<profile>/workflows/anomalous_materials/
|
||||
11. 删除素材时同时删除其私有图片资产,不影响原始输出图。
|
||||
12. 图库详情的每个节点卡直接提供“存入素材库”;勾选多个节点后,节点列表顶部提供“存入素材库 (N)”。底部只保存完整图片与工作流,不再用范围下拉框混合两类操作。节点参数素材不提供误导性的“打开完整工作流”。
|
||||
13. 详情缓存最多保留 16 张的解析结果;正常情况下直接使用后端已验证的 workflow,不在浏览器中再次读取整张 PNG。旧后端兜底读取可随导航中止,且不缓存重复的 workflow/节点完整值;本地模型缩略图匹配只在打开“模型资产”页时运行。
|
||||
14. 素材库卡片可原地展开查看模型引用和其实际可用的节点参数;完整素材展示全部节点,节点选择素材只展示已保存的节点。闭合卡片时释放完整响应和详细 DOM。
|
||||
|
||||
## 七、明确未纳入本次 MVP 的内容
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user