diff --git a/CHANGELOG.md b/CHANGELOG.md
index c672b39..0605388 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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).
diff --git a/web/modules/ui_combo_structure.js b/web/modules/ui_combo_structure.js
index ccedf43..6a47cc7 100644
--- a/web/modules/ui_combo_structure.js
+++ b/web/modules/ui_combo_structure.js
@@ -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.
diff --git a/web/modules/ui_notebook_editor.js b/web/modules/ui_notebook_editor.js
index f720aac..d442c7a 100644
--- a/web/modules/ui_notebook_editor.js
+++ b/web/modules/ui_notebook_editor.js
@@ -511,7 +511,7 @@ export function fillNotebookGalleries(baseModel, mainGallery, loraGallery, data)
if (isVid) return ``;
return ``;
}
- return `