AMB only uses: training goes to its own plugin. This takes back the record of what works (api/ai_evidence.py), the training network and rounds (api/ai_training.py, ui_ai_network*.js, the round form), the Gates' record switch, the ways' order by record and the external_skill field of log_outside that only fed the record. Kept, as part of using AMB: - the user's word under a finished run, now only told to the AI (ai_run_control.set_verdict, ui_ai_verdict.js); - the Gates' "Custom nodes: only the ways'" rule; - a picks block left to the AI takes its recommended picture. Reverts73518dc,67bf5b2,6647b05and49d05cf, then puts those three back with tests in test_ai_run_watch, test_ai_gates and test_ai_panels. Full suite and structure check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
9.5 KiB
JavaScript
188 lines
9.5 KiB
JavaScript
/**
|
|
* The side column's Ways view (做法, ui_ai_side_views.js): the ready-made blocks Anomalous builds for
|
|
* the AI (api/reference_blocks.py, a data file a block), by use: what each does, and under Details where it came
|
|
* from, in the user's language; greyed with what it lacks when a node pack or a model is missing
|
|
* (a pack opens ComfyUI-Manager's list searched for it); and a switch the user turns a block off
|
|
* with, so the AI is not offered it; how it was tried (the card, a run's time) and the models it recommends
|
|
* (api/block_models.json), each here or a button downloading it, link and SHA256 from the list.
|
|
* Under them the skills (api/ai_skills.py): advice the AI fetches for a kind of job, each with its
|
|
* switch too; for a private task only the private task's skill, as that is all its AI gets. GET /anomalous/ai_blocks, POST {name, on} or {skill, on}. Drawn when shown,
|
|
* the list read again each time the view is opened.
|
|
*/
|
|
|
|
import { resolveLocale, translate as t } from './locales.js';
|
|
import { sideHead } from './ui_ai_side.js';
|
|
import { openManager } from './ui_pack_usage.js';
|
|
import { downloadFrom } from './model_download.js';
|
|
import { formatSize } from './file_size.js';
|
|
|
|
let reading = null; // the list being read
|
|
let shown = null; // { blocks, jobs } last read, or { error }
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = String(text);
|
|
return node;
|
|
}
|
|
|
|
const words = value => (resolveLocale() === 'zh' ? value?.zh : value?.en) || value?.en || value?.zh || '';
|
|
|
|
async function ask(body) {
|
|
const answer = await fetch('/anomalous/ai_blocks', body ? {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
} : undefined);
|
|
const data = await answer.json().catch(() => ({}));
|
|
if (!answer.ok) throw new Error(data.error || answer.statusText);
|
|
return data;
|
|
}
|
|
|
|
/** What the block lacks, a line each: its packs (each a way into the Manager) and its models. */
|
|
function lacks(block, owner) {
|
|
const box = el('div', 'anomalous-ai-block-lacks');
|
|
for (const pack of block.packs.filter(item => !item.installed)) {
|
|
const node = el('button', 'anomalous-ai-block-lack is-pack', t('aiBlocksNeedsPack', { name: pack.name }));
|
|
node.type = 'button';
|
|
node.title = t('aiBlocksFindPack');
|
|
node.onclick = () => void openManager(owner, { name: pack.name });
|
|
box.append(node);
|
|
}
|
|
for (const model of block.models) {
|
|
box.append(el('span', 'anomalous-ai-block-lack', t(`aiBlocksNeeds_${model.kind}`, { what: t(`aiBlocksWhat_${model.what || 'none'}`) })));
|
|
}
|
|
return box;
|
|
}
|
|
|
|
/** How the block was tried: the card, and a whole run's time for each model and size. */
|
|
function tested(block) {
|
|
const record = block.tested || {};
|
|
const runs = (record.runs || []).filter(run => run.seconds).map(run => t('aiBlocksRun', run));
|
|
return runs.length ? el('div', 'anomalous-ai-block-source', t('aiBlocksTested', { gpu: record.gpu, vram: record.vram_gb, runs: runs.join(' · ') })) : null;
|
|
}
|
|
|
|
/** The models the block recommends: each one here, or a button that downloads it (its link and SHA256 from the list). */
|
|
function recommended(block, owner) {
|
|
if (!block.recommended?.length) return null;
|
|
const box = el('div', 'anomalous-ai-block-models');
|
|
box.append(el('span', 'anomalous-ai-block-source', t('aiBlocksModels')));
|
|
for (const model of block.recommended) {
|
|
const about = t('aiBlocksModelAbout', { license: model.license, folder: [model.folder, model.subfolder].filter(Boolean).join('/'), base: model.base || '—' });
|
|
if (model.installed) {
|
|
const here = el('span', 'anomalous-ai-block-model is-here', `✓ ${model.title}`);
|
|
here.title = about;
|
|
box.append(here);
|
|
continue;
|
|
}
|
|
const get = el('button', 'anomalous-ai-block-model', t('aiBlocksDownload', { name: model.title, size: formatSize(model.size) }));
|
|
get.type = 'button';
|
|
get.title = about;
|
|
get.onclick = async () => {
|
|
get.disabled = true;
|
|
try {
|
|
await downloadFrom(owner, { url: model.sources[0].url, hash: model.sha256, type: model.folder, folder: model.subfolder, name: model.file });
|
|
get.textContent = t('aiBlocksDownloading', { name: model.title });
|
|
} catch (error) {
|
|
get.disabled = false;
|
|
get.textContent = t('aiBlocksDownloadFailed', { name: model.title, error: error.message });
|
|
}
|
|
};
|
|
box.append(get);
|
|
}
|
|
return box;
|
|
}
|
|
|
|
/** The switch that turns `item` on or off for the AI; `key`: {name} for a block, {skill} for a skill. */
|
|
function onSwitch(item, key, redraw) {
|
|
const toggle = el('input', 'anomalous-ai-block-switch');
|
|
toggle.type = 'checkbox';
|
|
toggle.setAttribute('role', 'switch');
|
|
toggle.checked = item.on;
|
|
toggle.title = t(item.on ? 'aiBlocksOnHint' : 'aiBlocksOffHint');
|
|
toggle.setAttribute('aria-label', t('aiBlocksSwitch', { name: words(item.title) }));
|
|
toggle.onchange = async () => {
|
|
toggle.disabled = true;
|
|
try {
|
|
shown = await ask({ ...key, on: toggle.checked });
|
|
} catch (error) {
|
|
toggle.checked = !toggle.checked;
|
|
shown = { ...shown, note: error.message };
|
|
}
|
|
redraw();
|
|
};
|
|
return toggle;
|
|
}
|
|
|
|
function blockRow(block, owner, redraw) {
|
|
const row = el('div', `anomalous-ai-block${block.usable ? '' : ' is-missing'}${block.on ? '' : ' is-off'}`);
|
|
const body = el('div', 'anomalous-ai-block-body');
|
|
const head = el('div', 'anomalous-ai-block-head');
|
|
head.append(el('span', 'anomalous-ai-block-title', words(block.title)),
|
|
el('span', `anomalous-ai-block-state${block.usable ? ' is-ready' : ''}`, t(block.usable ? 'aiBlocksReady' : 'aiBlocksMissing')));
|
|
body.append(head, el('div', 'anomalous-ai-block-about', words(block.about)));
|
|
if (!block.usable) body.append(lacks(block, owner));
|
|
// Where it came from, how it was tried, its models: folded, the row is a line to read.
|
|
const more = el('details', 'anomalous-ai-block-more');
|
|
more.append(el('summary', '', t('aiBlocksDetails')));
|
|
if (block.better_with?.length) more.append(el('div', 'anomalous-ai-block-source', t('aiBlocksBetterWith', { names: block.better_with.join(', ') })));
|
|
if (words(block.source)) more.append(el('div', 'anomalous-ai-block-source', t('aiBlocksSource', { source: words(block.source) })));
|
|
more.append(...[tested(block), recommended(block, owner)].filter(Boolean));
|
|
if (more.childElementCount > 1) body.append(more);
|
|
row.append(body, onSwitch(block, { name: block.name }, redraw));
|
|
return row;
|
|
}
|
|
|
|
/** The skills of the task's kind (`private`): a title, what it advises, its switch. */
|
|
function skillsGroup(skills, redraw, isPrivate) {
|
|
const group = el('div', 'anomalous-ai-blocks-group');
|
|
group.append(el('div', 'anomalous-ai-blocks-job', t('aiSkillsHeading')),
|
|
el('div', 'anomalous-ai-blocks-note', t(isPrivate ? 'aiSkillsNotePrivate' : 'aiSkillsNote')));
|
|
for (const skill of skills.filter(item => Boolean(item.private_only) === isPrivate)) {
|
|
const row = el('div', `anomalous-ai-block${skill.on ? '' : ' is-off'}`);
|
|
const body = el('div', 'anomalous-ai-block-body');
|
|
const head = el('div', 'anomalous-ai-block-head');
|
|
head.append(el('span', 'anomalous-ai-block-title', words(skill.title)));
|
|
body.append(head, el('div', 'anomalous-ai-block-about', words(skill.about)));
|
|
row.append(body, onSwitch(skill, { skill: skill.name }, redraw));
|
|
group.append(row);
|
|
}
|
|
return group;
|
|
}
|
|
|
|
function listBox(owner, redraw, isPrivate) {
|
|
const body = el('div', 'anomalous-ai-stage-body anomalous-ai-blocks');
|
|
if (!shown) {
|
|
body.append(el('div', 'anomalous-ai-stage-empty', t('aiBlocksReading')));
|
|
return body;
|
|
}
|
|
if (shown.error) {
|
|
body.append(el('div', 'anomalous-ai-stage-empty', t('aiBlocksUnread', { error: shown.error })));
|
|
return body;
|
|
}
|
|
const offered = shown.blocks.filter(block => block.usable && block.on).length;
|
|
body.append(el('div', 'anomalous-ai-blocks-note', t('aiBlocksNote', { count: offered, total: shown.blocks.length })));
|
|
if (shown.note) body.append(el('div', 'anomalous-ai-blocks-note is-error', shown.note));
|
|
for (const job of [...shown.jobs, ...new Set(shown.blocks.map(block => block.job))]) {
|
|
const blocks = shown.blocks.filter(block => block.job === job);
|
|
if (!blocks.length || body.querySelector(`[data-job="${CSS.escape(job)}"]`)) continue;
|
|
const group = el('div', 'anomalous-ai-blocks-group');
|
|
group.dataset.job = job;
|
|
group.append(el('div', 'anomalous-ai-blocks-job', t(`aiBlocksJob_${job}`)), ...blocks.map(block => blockRow(block, owner, redraw)));
|
|
body.append(group);
|
|
}
|
|
if (shown.skills?.length) body.append(skillsGroup(shown.skills, redraw, isPrivate));
|
|
return body;
|
|
}
|
|
|
|
/**
|
|
* The view into `pane`; `ctx`: {close, owner}; `isPrivate`: the task shown is private (its skills only).
|
|
* `fresh`: the view was just opened, so the list is read again.
|
|
*/
|
|
export function renderBlocksView(pane, ctx, fresh = false, isPrivate = false) {
|
|
const redraw = () => pane.replaceChildren(sideHead(t('aiSideBlocks'), ctx.close), listBox(ctx.owner, redraw, isPrivate));
|
|
if ((fresh || !shown) && !reading) {
|
|
reading = ask().then(data => { shown = data; }, error => { shown = { error: error.message }; })
|
|
.finally(() => { reading = null; redraw(); });
|
|
}
|
|
redraw();
|
|
}
|