Combo models as cover cards, not rows as wide as the window

A combo saved from picked nodes shows each model as a card like the model
library's: the cover with its slot written on it, the name and node below,
a click on the card (or Change on the cover) picks another model. The
default combo's main models and LoRAs to pick from are cover cards side by
side instead of wide rows; its empty cover is a class, not inline styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-17 03:21:44 +08:00
co-authored by Claude Opus 5.5
parent db0067daea
commit e05e4e24fa
6 changed files with 130 additions and 23 deletions
+3
View File
@@ -8,6 +8,9 @@
- **Before an online scan, the plugin checks whether Civitai can be reached**, on both its addresses. When it cannot, it asks instead of waiting on every model: **Scan from the files** (base models found, no covers or trigger words; a later scan fills them in), **Set a proxy** (opens Settings → Models → Network), or Cancel. A check that reached Civitai is trusted for five minutes.
- **Civitai's other address (civitai.red) when civitai.com does not answer**: some networks reach only the second, a cloud server in mainland China among them (tested on AutoDL: civitai.com timed out, civitai.red answered in about a second). Scans, import and Model Check's lookups and **Settings → Network → Test** ask civitai.red then, so models still get their names, trigger words and base models; the test says which address answered. The covers are on Civitai's picture server, which such networks often cannot reach either: after two covers in a row that would not download, a scan fetches no more covers instead of waiting on each.
### 🧩 Combos (搭配)
- **A combo's models are cover cards**, as in the model library: the cover on top, the name below. In a combo saved from picked nodes, the slot (Main model, LoRA…) is written on the cover and a click on the card changes the model; in a default combo, the main models and LoRAs to pick from are cover cards side by side. They used to be rows as wide as the window, with **Change** far from the name (模型卡片不再横向拉满整个窗口).
### ☁️ ComfyUI on a cloud server (云服务器上使用)
- **How far it goes**: fully supported on overseas cloud servers (RunPod…). On servers in mainland China (AutoDL…) only in part: browsing, offline scans and tidying work, but Civitai is mostly out of reach there, so lookups are unreliable and covers rarely download (海外服务器完整支持;国内服务器只能部分支持). Not fully tested yet: one AutoDL server so far, no overseas one. The cloud guide ends with a section for AI assistants (what is going on, the steps, how to check, what to do when it did not work, what to tell the user about safety), so a user can hand the link to their AI.
- On a rented cloud server (AutoDL, RunPod…) the page is always opened from another device, so since v1.58.1 the plugin only let it look: scans, downloads, moves and saves were refused. Now there are two ways: open ComfyUI through an **SSH tunnel** (the address starts with 127.0.0.1; it counts as the computer ComfyUI runs on, nothing to change), or make an empty file named **`allow_remote_changes`** in the plugin folder on the server and restart ComfyUI. With the file, every device that can open the page may change things; the page can neither make nor remove it, and the MCP endpoint stays for this computer only. How, and the risk: [docs/guides/cloud-server.md](docs/guides/cloud-server.md).
+30 -9
View File
@@ -361,12 +361,18 @@ export function renderStructuredCombo(owner, host, note, toolbar) {
return [slot.id, slotLabel({ ...slot, index: counts[slot.folder] > 1 ? seen[slot.folder] : 0 })];
}));
// Which node a slot is on, and its node pack when it is not ComfyUI's own.
const nodeName = (slot) => {
const node = nodeOf.get(slot.node);
if (!node) return null;
return { text: `${node.title || node.name || node.type}${node.pack ? `(${node.pack})` : ''}`,
title: node.pack ? `${node.type} · ${node.pack}` : node.type };
};
const labelLine = (slot) => {
const line = el('span', 'anomalous-structure-label', labels.get(slot.id));
const node = nodeOf.get(slot.node);
const node = nodeName(slot);
if (node) {
line.append(el('span', 'anomalous-structure-node', ` · ${node.title || node.name || node.type}${node.pack ? `(${node.pack})` : ''}`));
line.title = node.pack ? `${node.type} · ${node.pack}` : node.type;
line.append(el('span', 'anomalous-structure-node', ` · ${node.text}`));
line.title = node.title;
}
return line;
};
@@ -392,34 +398,43 @@ export function renderStructuredCombo(owner, host, note, toolbar) {
save(true);
row.replaceWith(drawModel(slot));
};
// A model is a cover card, like the model library's: its slot on the cover, a click on it changes the model.
const drawModel = (slot) => {
const row = el('div', 'anomalous-structure-row is-model');
row.dataset.slot = slot.id;
const value = data.values[slot.id];
const cover = el('div', 'anomalous-structure-cover');
cover.append(drawThumb(slot), el('span', 'anomalous-structure-badge', labels.get(slot.id)));
const copy = el('div', 'anomalous-structure-copy');
copy.append(labelLine(slot),
el('strong', 'anomalous-structure-value', isNone(value) ? t('comboSlotEmpty') : shortName(value)));
copy.append(el('strong', 'anomalous-structure-value', isNone(value) ? t('comboSlotEmpty') : shortName(value)));
if (!isNone(value)) copy.title = value;
const node = nodeName(slot);
if (node) {
const line = el('span', 'anomalous-structure-node', node.text);
line.title = node.title;
copy.append(line);
}
// The node's other values, as saved: shown, not changed here (a combo puts down exactly what was saved).
const fixed = fixedValues(nodeOf.get(slot.node), structure.slots, slot);
if (fixed.length) copy.append(chipLine(fixed));
const actions = el('div', 'anomalous-structure-actions');
if (slot.folder) {
actions.append(button('anomalous-scan-row-btn is-main', t('comboPick'), () => pick(slot, row)));
cover.append(button('anomalous-structure-change', t('comboPick'), () => pick(slot, row)));
row.classList.add('is-pickable');
row.onclick = (event) => { if (!event.target.closest('button')) void pick(slot, row); };
} else {
copy.append(el('span', 'anomalous-structure-note is-muted', t('comboSlotUnknownFolder')));
}
if (slot.optional && !isNone(value)) {
const actions = el('div', 'anomalous-structure-actions');
actions.append(button('anomalous-scan-row-btn', t('comboSlotClear'), () => {
data.values[slot.id] = 'None';
delete data.covers[slot.id];
save(true);
row.replaceWith(drawModel(slot));
}));
copy.append(actions);
}
row.append(drawThumb(slot), copy, actions);
row.append(cover, copy);
return row;
};
const fits = [];
@@ -459,7 +474,13 @@ export function renderStructuredCombo(owner, host, note, toolbar) {
const section = el('div', 'anomalous-structure-group');
const heading = el('h4', 'anomalous-structure-heading', t(key));
if (slots.length > 1) heading.append(el('span', 'anomalous-structure-count', String(slots.length)));
section.append(heading, ...slots.map(slot => (slot.kind === 'text' ? drawText(slot) : drawModel(slot))));
if (key === 'comboGroupPrompts') {
section.append(heading, ...slots.map(drawText));
} else {
const covers = el('div', 'anomalous-structure-models');
covers.append(...slots.map(drawModel));
section.append(heading, covers);
}
card.append(section);
}
// Nodes without slots go down exactly as saved: their name and saved values, nothing to change here.
+1 -1
View File
@@ -511,7 +511,7 @@ export function fillNotebookGalleries(baseModel, mainGallery, loraGallery, data)
if (isVid) return `<video src="${m.preview_url}" muted loop playsinline></video>`;
return `<img src="${m.preview_url}" />`;
}
return `<div style="width:30px; height:30px; background:var(--amb-bg-card); border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; color:var(--amb-text-dim);">?</div>`;
return '<div class="anomalous-nb-mini-empty">?</div>';
};
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=checkpoints,unet,diffusion_models`)
+2 -2
View File
@@ -1,6 +1,6 @@
@import url("./styles/00-foundation-models.css?v=20261014-folders-1");
@import url("./styles/01-shell-tools.css?v=20261014-folders-1");
@import url("./styles/02-notebooks.css?v=20261014-loranode-1");
@import url("./styles/02-notebooks.css?v=20261017-covers");
@import url("./styles/03-gallery-recipes.css?v=20261014-file-2");
@import url("./styles/04-dialogs-materials.css?v=20261011-wfd-3");
@import url("./styles/05-recipe-studio-overrides.css?v=20261011-wfd-3");
@@ -21,7 +21,7 @@
@import url("./styles/20-current-node.css?v=20261011-last");
@import url("./styles/21-prompt-studio.css?v=20261007-theme-2");
@import url("./styles/22-model-download.css?v=20261007-theme-2");
@import url("./styles/23-combo-structure.css?v=20261007-theme-2");
@import url("./styles/23-combo-structure.css?v=20261017-covers");
@import url("./styles/24-backup.css?v=20261007-theme-2");
@import url("./styles/25-model-import.css?v=20261008-import");
@import url("./styles/26-model-tidy.css?v=20261008-tidy");
+22 -10
View File
@@ -476,18 +476,17 @@
overflow: visible;
}
/* Cover cards, as in the model library: the cover on top, the name below. */
.anomalous-nb-minicheck,
.anomalous-nb-minilora {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px 8px 8px;
flex: 0 0 120px;
flex-direction: column;
width: 120px;
background: var(--amb-bg-card-hover);
border: 1px solid var(--amb-border);
border-radius: 8px;
cursor: pointer;
flex: 1 1 220px;
max-width: 320px;
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@@ -510,15 +509,25 @@
.anomalous-nb-minicheck img,
.anomalous-nb-minicheck video,
.anomalous-nb-minilora img,
.anomalous-nb-minilora video {
width: 52px;
height: 52px;
aspect-ratio: 1 / 1;
.anomalous-nb-minilora video,
.anomalous-nb-mini-empty {
width: 100%;
height: auto;
aspect-ratio: 3 / 4;
object-fit: cover;
border-radius: 6px;
border-radius: 7px 7px 0 0;
flex-shrink: 0;
}
.anomalous-nb-mini-empty {
display: flex;
align-items: center;
justify-content: center;
background: var(--amb-bg-card);
color: var(--amb-text-dim);
font-size: 12px;
}
.anomalous-nb-minicheck-name,
.anomalous-nb-minilora-name {
color: var(--amb-text-main);
@@ -529,7 +538,10 @@
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
padding: 6px 8px 7px;
font-size: 12px;
text-align: left;
word-break: break-word;
}
.anomalous-nb-minicheck.selected .anomalous-nb-minicheck-name,
+72 -1
View File
@@ -204,7 +204,6 @@
line-height: 18px;
}
.anomalous-structure-row.is-model,
.anomalous-structure-row.is-fixed {
display: flex;
align-items: center;
@@ -215,9 +214,81 @@
background: var(--amb-bg-card);
}
/* Models: cover cards side by side, as in the model library. */
.anomalous-structure-models {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.anomalous-structure-row.is-model {
display: flex;
flex-direction: column;
width: 150px;
overflow: hidden;
border: 1px solid var(--amb-border);
border-radius: 10px;
background: var(--amb-bg-card);
}
.anomalous-structure-row.is-pickable { cursor: pointer; }
.anomalous-structure-row.is-pickable:hover { border-color: var(--amb-link); }
.anomalous-structure-cover { position: relative; }
.anomalous-structure-row.is-model .anomalous-structure-thumb {
width: 100%;
height: auto;
aspect-ratio: 3 / 4;
border-radius: 0;
}
/* Text on the cover: dark plates, readable on any picture and in either theme. */
.anomalous-structure-badge {
position: absolute;
top: 6px;
left: 6px;
max-width: calc(100% - 12px);
overflow: hidden;
padding: 1px 7px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.62);
color: #fff;
font-size: 11px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.anomalous-structure-change {
position: absolute;
bottom: 8px;
left: 50%;
padding: 3px 14px;
border: 0;
border-radius: 999px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font: inherit;
font-size: 12px;
opacity: 0;
transform: translateX(-50%);
transition: opacity 0.15s;
cursor: pointer;
}
.anomalous-structure-row.is-model:hover .anomalous-structure-change,
.anomalous-structure-change:focus-visible { opacity: 1; }
@media (hover: none) {
.anomalous-structure-change { opacity: 1; }
}
.anomalous-structure-row.is-model .anomalous-structure-copy { padding: 6px 8px 8px; }
.anomalous-structure-row.is-model .anomalous-structure-value { font-size: 13px; }
.anomalous-structure-row.is-model .anomalous-structure-node { font-size: 11px; }
.anomalous-structure-row.is-model .anomalous-structure-actions { margin-top: 4px; }
.anomalous-structure-row.is-text {
display: flex;
flex-direction: column;