diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 61fdac2..89ab9c3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -430,7 +430,7 @@ covered by the `styles.css` manifest. notebook or material delete route); `image_keep.js` does the keeping (an image as a combo, its prompts as a saved prompt, reading what was kept) without DOM. - `ui_combos.js` renders the Combos page's list (搭配: a main model, LoRAs and a prompt, formerly Prompt Notes): search, - New, and a card per combo with its model's cover and Put on canvas; a card opens the combo's editor, and dragged onto the + ? (what a combo is), New, and a card per combo with its model's cover and Put on canvas; a card opens the combo's editor, and dragged onto the canvas (`material_drag.js`) becomes a new group of nodes where it is dropped. Combos keep the notes' files. A combo can instead hold a node structure (`data.kind === 'nodes'`): `combo_structure.js` captures the canvas's picked nodes (subgraphs aside) with the links between them and puts @@ -488,7 +488,7 @@ covered by the `styles.css` manifest. `ui_image_inspector.js`. - `ui_recipes.js` / `ui_recipe_detail.js` and `ui_notebooks.js` own their respective workspace surfaces and persistence flows. `ui_recipes.js` hands the - Workflows page (its catalog) the recipe services (save, edit, fetch) and opens a recipe by name. `ui_notebooks.js` owns the Combos workspace (its list or one combo's editor, + Workflows page (its catalog) the recipe services (save, edit, fetch) and opens a recipe by name. `ui_notebooks.js` owns the Combos workspace (filling the page; its list or one combo's editor, with Back to the list) and combo persistence; `ui_notebook_editor.js` owns unfolded card editing (modularized into single-responsibility sub-functions adhering to the 50-line rule: sticky top action toolbar with floating More popover dropdown and timed two-step delete safety guard, unfolded companion models card with unconstrained multi-column tile flow eliminating nested gallery scrollbars, diff --git a/CHANGELOG.md b/CHANGELOG.md index 1465bcd..00d7cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ - **⋯** holds Copy path, Copy file name and **Delete**, which is no longer a red button next to Add node; it asks first in AMB's own dialog, and the files still go to the Recycle Bin. - The file's full path shows once, in a line under the buttons with **Copy**. The ⬇ (jump to the bottom) button is gone. - In a narrow window (the browser docked beside the canvas) the cover column is hidden as before, so a small cover now sits beside the name. +- A model's description (Civitai's own text) keeps its headings a little above the text instead of poster size. + +### 🗂️ The folder column follows the type; open folders in Explorer (文件夹栏跟着类型走,一键打开文件夹) +- The column left of the models used to list every type's folders (raw names like `checkpoints (1)`, `[Root]` rows with 0, nothing marking where you were), repeating the chips above it. Now it shows the folders of the type you picked: **All**, then its subfolders as a tree whose numbers include their subfolders (empty ones dimmed, ▾ folds one), and **In the top folder** for models lying directly in the type's folder. The folder you are looking at is marked. +- **📂 Open** next to the type's name opens that type's folder in Explorer, and a 📂 on any folder you point at opens that one, so you can drop files in or tidy up by hand. Like the details' Folder button, it works only on the computer ComfyUI runs on. + +### 🎨 The Combos page looks like the others (搭配页和其他页面统一) +- It no longer sits in a lighter, inset box with a shadow, a second "Combos" title and a red ✕: it fills the page in the same colours as Models and Workflows. The explanation of what a combo is folds behind **?** (shown by itself while you have no combos), and the editor's Back reads **← All combos**. ### ≈ What was read from the file says so (离线推断标清楚) - A model Civitai does not know has its base model inferred from the file, as before; now it says so. Its card reads **推断 SDXL** (it was ≈), and its details open with a note: why Civitai did not have it, that the base model was read from the file and may be off, and **Add a name and source link**, instead of an English placeholder description. diff --git a/web/modules/locales.js b/web/modules/locales.js index 0e7c575..e91140d 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -552,6 +552,7 @@ export const i18n = { baseModel: '基础模型', mainModel: '主模型', shellCombos: "搭配", + comboHelp: "搭配是什么?", shellPrompts: "提示词", comboLead: "搭配 = 选哪些模型 + 写什么提示词。默认是一个主模型 + 几个 LoRA + 提示词;也可以把画布上选中的一段节点存下来(Flux、视频流程,放大、ControlNet 这类几个节点配合的也行),模型和提示词以后能换,其他节点原样放回。按“放到画布”就连好线放上去,也可以把卡片拖到画布上。", comboDragHint: "拖到画布上松手,就在那里放一组连好线的节点;Esc 取消", @@ -629,7 +630,7 @@ export const i18n = { comboEmpty: "还没有搭配。点“新建搭配”,选一个主模型和几个 LoRA,写好提示词;以后按一下“放到画布”就能连好线放出来。", comboNoMatches: "没有符合的搭配。", comboLoadError: "搭配读取失败,请重试。", - comboBack: "‹ 全部搭配", + comboBack: "← 全部搭配", noteNeedsClip: "UNet 模型不带文本编码器:画布上没有唯一的 CLIP 加载节点,提示词节点的 clip 要你自己接。", translate: '翻译', noBaseModel: '请先选择 Base Model', @@ -3011,6 +3012,7 @@ export const i18n = { baseModel: 'Base Model', mainModel: 'Main Model', shellCombos: "Combos", + comboHelp: "What is a combo?", shellPrompts: "Prompts", comboLead: "A combo is which models and which prompts. By default a main model, a few LoRAs and a prompt; or keep a piece of the canvas, the nodes you picked (Flux and video workflows, upscaling, ControlNet and other nodes that work together): its models and prompts can be changed later, the other nodes go back as saved. Put on canvas lays them out wired together; you can also drag a card onto the canvas.", comboDragHint: "Release on the canvas for a wired group of nodes there; Esc cancels", @@ -3088,7 +3090,7 @@ export const i18n = { comboEmpty: "No combos yet. Press New combo, pick a main model and some LoRAs and write a prompt; then one press of Put on canvas lays them out, wired.", comboNoMatches: "No combo matches.", comboLoadError: "Could not load the combos; try again.", - comboBack: "‹ All combos", + comboBack: "← All combos", noteNeedsClip: "A UNet model has no text encoder and the canvas has no single CLIP loader, so connect the prompt nodes' clip yourself.", translate: 'Translate', noBaseModel: 'Select a Base Model first', diff --git a/web/modules/ui_combos.js b/web/modules/ui_combos.js index a370fcd..4bc6ddf 100644 --- a/web/modules/ui_combos.js +++ b/web/modules/ui_combos.js @@ -102,15 +102,27 @@ export async function newCombo(owner) { if (await owner.saveCurrentNotebook()) owner.openCombo(note); } -/** Fills `host` with the search box, New and a card per combo matching the search. */ +/** + * Fills `host` with the search box, ? (what a combo is; shown by itself while there are none), + * New and a card per combo matching the search. + */ export async function renderComboList(owner, host) { const bar = el('div', 'anomalous-combo-bar'); const search = el('input', 'anomalous-combo-search'); search.type = 'search'; search.placeholder = t('comboSearch'); search.value = owner.comboQuery || ''; - bar.append(el('p', 'anomalous-combo-lead', t('comboLead')), search, - button('anomalous-btn-primary', t('comboNew'), () => void chooseNewCombo(owner, () => newCombo(owner)))); + const lead = el('p', 'anomalous-combo-lead', t('comboLead')); + lead.hidden = true; + const help = button('anomalous-combo-help', '?', () => { + lead.hidden = !lead.hidden; + help.setAttribute('aria-expanded', String(!lead.hidden)); + }); + help.title = t('comboHelp'); + help.setAttribute('aria-label', t('comboHelp')); + help.setAttribute('aria-expanded', 'false'); + bar.append(search, help, + button('anomalous-btn-primary', t('comboNew'), () => void chooseNewCombo(owner, () => newCombo(owner))), lead); const grid = el('div', 'anomalous-combo-grid'); host.replaceChildren(bar, grid); grid.append(el('p', 'anomalous-combo-empty', t('loading'))); @@ -118,6 +130,7 @@ export async function renderComboList(owner, host) { let combos; try { combos = await listCombos(); + if (!combos.length) lead.hidden = false; } catch (error) { console.error('[AMB] Could not list combos:', error); grid.replaceChildren(el('p', 'anomalous-combo-empty', t('comboLoadError'))); diff --git a/web/modules/ui_detail.js b/web/modules/ui_detail.js index ec94010..1e70913 100644 --- a/web/modules/ui_detail.js +++ b/web/modules/ui_detail.js @@ -316,6 +316,7 @@ export function showDetail(model) { descCont.appendChild(descLabel); const descText = document.createElement('div'); + descText.className = 'anomalous-detail-description'; // Civitai's headings kept to size descText.style.flex = 'none'; descText.style.background = 'var(--amb-bg-card)'; diff --git a/web/modules/ui_notebooks.js b/web/modules/ui_notebooks.js index 5b59fea..28caa9a 100644 --- a/web/modules/ui_notebooks.js +++ b/web/modules/ui_notebooks.js @@ -1,6 +1,7 @@ /** - * The Combos page (搭配) as a workspace over the current page: the list of combos - * (ui_combos.js), or one combo in its editor (ui_notebook_editor.js) with Back to the list; + * The Combos page (搭配) as a workspace over the current page, filling it like Workflows: the + * list of combos (ui_combos.js), or one combo in its editor (ui_notebook_editor.js) under a bar + * with Back to the list (Esc presses it; the rail and Esc close the page); * saving and deleting a combo (the files under workflows/anomalous_notebooks, which the * code still calls notebooks: combos were Prompt Notes before). */ @@ -13,22 +14,14 @@ const t = (key, params) => translate(key, params); function buildComboWorkspace(owner) { const container = document.createElement('div'); - container.className = 'anomalous-nb-container'; - const header = document.createElement('div'); - header.className = 'anomalous-nb-header'; - const headerMain = document.createElement('div'); - headerMain.className = 'anomalous-nb-header-main'; + container.className = 'anomalous-nb-container anomalous-combo-container'; + owner.comboHeader = document.createElement('div'); + owner.comboHeader.className = 'anomalous-nb-header'; owner.comboBack = document.createElement('button'); owner.comboBack.type = 'button'; owner.comboBack.className = 'anomalous-btn-ghost anomalous-nb-back'; owner.comboBack.onclick = () => owner.showNotebooks(); - owner.comboHeading = document.createElement('h2'); - headerMain.append(owner.comboBack, owner.comboHeading); - const close = document.createElement('span'); - close.className = 'anomalous-nb-close'; - close.innerHTML = '×'; - close.onclick = () => owner.closeWorkspace(); - header.append(headerMain, close); + owner.comboHeader.append(owner.comboBack); const body = document.createElement('div'); body.className = 'anomalous-nb-body'; @@ -37,7 +30,7 @@ function buildComboWorkspace(owner) { owner.nbEditor = document.createElement('div'); owner.nbEditor.className = 'anomalous-nb-editor'; body.append(owner.comboList, owner.nbEditor); - container.append(header, body); + container.append(owner.comboHeader, body); owner.nbPanel.appendChild(container); owner.notebookContainer = container; owner.notebookBody = body; @@ -68,9 +61,8 @@ export async function showNotebooks({ editor = false } = {}) { this.markWorkspace?.('combos'); const editing = editor && Boolean(this.currentNotebook); - this.comboHeading.textContent = t('shellCombos'); this.comboBack.textContent = t('comboBack'); - this.comboBack.hidden = !editing; + this.comboHeader.hidden = !editing; this.comboList.hidden = editing; this.nbEditor.hidden = !editing; if (editing) { diff --git a/web/styles.css b/web/styles.css index d56c92f..09da40b 100644 --- a/web/styles.css +++ b/web/styles.css @@ -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=20261012-click-2"); +@import url("./styles/02-notebooks.css?v=20261014-combos-1"); @import url("./styles/03-gallery-recipes.css?v=20261013-detail-1"); @import url("./styles/04-dialogs-materials.css?v=20261011-wfd-3"); @import url("./styles/05-recipe-studio-overrides.css?v=20261011-wfd-3"); @@ -29,4 +29,4 @@ @import url("./styles/28-workflows.css?v=20261010-wf-2"); @import url("./styles/29-workflow-detail.css?v=20261011-wfd-3"); @import url("./styles/30-scan-picker.css?v=20261011-scan"); -@import url("./styles/31-model-info.css?v=20261013-detail-1"); +@import url("./styles/31-model-info.css?v=20261014-desc-1"); diff --git a/web/styles/02-notebooks.css b/web/styles/02-notebooks.css index 989c227..0338f02 100644 --- a/web/styles/02-notebooks.css +++ b/web/styles/02-notebooks.css @@ -36,12 +36,6 @@ color: var(--amb-text-main); } -.anomalous-nb-close { - font-size: 2em; - cursor: pointer; - color: var(--amb-lt-danger, #ff4444); -} - .anomalous-nb-body { flex: 1; display: flex; @@ -49,7 +43,28 @@ container-type: inline-size; } -/* The Combos page (ui_combos.js): a bar with the search and New, then a card per combo. */ +/* The Combos workspace fills the page in its colour, like Workflows; its bar holds only the + editor's Back. */ +.anomalous-nb-container.anomalous-combo-container { + width: 100%; + height: 100%; + border: 0; + border-radius: 0; + background: var(--amb-bg-page); + box-shadow: none; +} + +.anomalous-combo-container > .anomalous-nb-header { + height: auto; + padding: 10px 20px; + background: transparent; +} + +.anomalous-combo-container > .anomalous-nb-header[hidden] { + display: none; +} + +/* The Combos page (ui_combos.js): a bar with the search, ? and New, then a card per combo. */ .anomalous-combo-page { flex: 1; display: flex; @@ -73,6 +88,29 @@ gap: 10px; } +.anomalous-combo-help { + flex: none; + width: 32px; + height: 32px; + border: 1px solid var(--amb-border-strong); + border-radius: 50%; + background: transparent; + color: var(--amb-text-soft); + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.anomalous-combo-help:hover, +.anomalous-combo-help[aria-expanded="true"] { + background: rgba(var(--amb-ink-rgb), 0.08); + color: var(--amb-text-main); +} + +.anomalous-combo-lead[hidden] { + display: none; +} + .anomalous-combo-lead { flex: 1 1 100%; margin: 0; diff --git a/web/styles/31-model-info.css b/web/styles/31-model-info.css index 35bfd6e..32d7395 100644 --- a/web/styles/31-model-info.css +++ b/web/styles/31-model-info.css @@ -493,6 +493,11 @@ color: var(--amb-bg-page); } +/* A model's description is Civitai's HTML; its headings stay a little above the text, not poster size. */ +.anomalous-detail-description :is(h1, h2) { margin: 0.8em 0 0.4em; font-size: 1.15em; line-height: 1.35; } +.anomalous-detail-description :is(h3, h4, h5, h6) { margin: 0.7em 0 0.3em; font-size: 1.05em; line-height: 1.35; } +.anomalous-detail-description > :first-child { margin-top: 0; } + .anomalous-detail-action.is-primary:hover { background: color-mix(in srgb, var(--amb-link) 85%, var(--amb-text-main)); } /* A model Civitai does not know: what was read from the file, and a way to add the rest. */