- model_kind: Anima LLLite, Qwen-Image block-wise and Z-Image Fun ControlNets (and other ModelPatchLoader files) go to model_patches, not controlnet; import and Tidy follow. - api/mcp_model_use.py: search_models / get_model say which node types load each model, or where a misfiled one belongs; move_model moves it there behind a new "files" gate (asks by default, never overwrites). - An empty drop-down in add_node says ComfyUI found no file; the task tab switch is no longer logged as the user's canvas change. - The folded task list stays while the pointer is in the box around its button and the list. - The message box's picture button picks gallery pictures; "@" offers the gallery's newest and searches it as you type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
6.6 KiB
JavaScript
140 lines
6.6 KiB
JavaScript
/**
|
||
* Gates on the AI workbench (api/ai_gates.py): the card under a step that waits for the user's OK
|
||
* (what the AI wants to do; Allow, No with a reason for the AI, and "from now on, go ahead"), what
|
||
* the user answered, and the Gates window where each kind of action is set to go ahead, ask first
|
||
* or never (changing the canvas, using the GPU, downloading, going online, moving model files), and whether a new
|
||
* task works in a canvas tab of its own (api/ai_task_canvas.py).
|
||
*/
|
||
|
||
import { translate as t } from './locales.js';
|
||
import { anomalousAlert } from './ui_dialog.js';
|
||
import { aiWindow } from './ui_ai_connect.js';
|
||
|
||
export const GATES = ['canvas', 'gpu', 'download', 'network', 'files'];
|
||
const POLICIES = ['auto', 'ask', 'never'];
|
||
|
||
function el(tag, className, text) {
|
||
const node = document.createElement(tag);
|
||
if (className) node.className = className;
|
||
if (text !== undefined) node.textContent = text;
|
||
return node;
|
||
}
|
||
|
||
function button(className, label, onClick, title) {
|
||
const node = el('button', className, label);
|
||
node.type = 'button';
|
||
node.onclick = onClick;
|
||
if (title) node.title = title;
|
||
return node;
|
||
}
|
||
|
||
async function post(url, body) {
|
||
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||
const data = await response.json().catch(() => ({}));
|
||
if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`);
|
||
return data;
|
||
}
|
||
|
||
/** What the user answered, or the card to answer with, under a step that has a gate; null otherwise. */
|
||
export function gateCard(step, taskId, changed) {
|
||
const asked = step.gate;
|
||
if (!asked || (!asked.lines && !asked.decision)) return null;
|
||
if (step.state !== 'asking') {
|
||
if (!asked.decision || asked.earlier) return null;
|
||
const words = asked.decision === 'allow'
|
||
? t(step.state === 'allowed' ? 'aiGateAllowedLater' : 'aiGateAllowed')
|
||
: t('aiGateDenied') + (asked.text ? `:${asked.text}` : '');
|
||
return el('div', `anomalous-ai-gate-answer is-${asked.decision}`, words);
|
||
}
|
||
const card = el('div', 'anomalous-ai-gate');
|
||
card.append(el('div', 'anomalous-ai-gate-title', t(`aiGateQ_${asked.gate}`)),
|
||
el('div', 'anomalous-ai-gate-why', t(`aiGateWhy_${asked.gate}`)));
|
||
if (asked.lines?.length) {
|
||
const lines = el('dl', 'anomalous-ai-gate-lines');
|
||
for (const line of asked.lines) {
|
||
const label = t(`aiGateArg_${line.label}`);
|
||
lines.append(el('dt', '', label === `aiGateArg_${line.label}` ? line.label : label), el('dd', '', line.value));
|
||
}
|
||
card.append(lines);
|
||
}
|
||
const reason = el('input', 'anomalous-ai-gate-reason');
|
||
reason.placeholder = t('aiGateReason');
|
||
reason.addEventListener('keydown', event => event.stopPropagation());
|
||
const remember = el('label', 'anomalous-ai-gate-remember');
|
||
const box = el('input');
|
||
box.type = 'checkbox';
|
||
remember.append(box, el('span', '', t('aiGateRemember', { gate: t(`aiGate_${asked.gate}`) })));
|
||
const answer = async (decision) => {
|
||
card.querySelectorAll('button').forEach((item) => { item.disabled = true; });
|
||
try {
|
||
await post(`/anomalous/ai_tasks/${encodeURIComponent(taskId)}/gate`,
|
||
{ step: step.id, decision, text: reason.value.trim(), remember: box.checked });
|
||
} catch (error) {
|
||
await anomalousAlert(error.message);
|
||
}
|
||
changed();
|
||
};
|
||
const actions = el('div', 'anomalous-ai-gate-actions');
|
||
actions.append(button('anomalous-ai-gate-allow', t('aiGateAllow'), () => answer('allow')),
|
||
button('anomalous-ai-gate-deny', t('aiGateDeny'), () => answer('deny')), remember);
|
||
card.append(reason, actions);
|
||
if (asked.timed_out) card.append(el('div', 'anomalous-ai-gate-note', t('aiGateLate')));
|
||
return card;
|
||
}
|
||
|
||
/** The Gates window: each kind of action the AI may do, and whether it goes ahead, asks or never. */
|
||
export async function openGates() {
|
||
const body = el('div', 'anomalous-ai-gates');
|
||
body.append(el('p', 'anomalous-ai-gates-intro', t('aiGatesIntro')));
|
||
let policies = {};
|
||
let canvasTab = 'new';
|
||
try {
|
||
const known = await (await fetch('/anomalous/ai_gates')).json();
|
||
policies = known.policies || {};
|
||
canvasTab = known.canvas_tab || 'new';
|
||
} catch (_) { /* drawn with nothing chosen; a change still saves */ }
|
||
for (const gate of GATES) {
|
||
const row = el('div', 'anomalous-ai-gates-row');
|
||
const words = el('div', 'anomalous-ai-gates-words');
|
||
words.append(el('div', 'anomalous-ai-gates-name', t(`aiGate_${gate}`)), el('div', 'anomalous-ai-gates-tools', t(`aiGateWhy_${gate}`)));
|
||
const choice = el('div', 'anomalous-ai-gates-choice');
|
||
choice.setAttribute('role', 'radiogroup');
|
||
for (const policy of POLICIES) {
|
||
const option = button(`anomalous-ai-gates-option${policies[gate] === policy ? ' is-on' : ''}`, t(`aiPolicy_${policy}`), async () => {
|
||
try {
|
||
policies = (await post('/anomalous/ai_gates', { gate, policy })).policies;
|
||
} catch (error) {
|
||
await anomalousAlert(error.message);
|
||
return;
|
||
}
|
||
choice.querySelectorAll('button').forEach((item, at) => item.classList.toggle('is-on', POLICIES[at] === policies[gate]));
|
||
});
|
||
option.setAttribute('role', 'radio');
|
||
choice.append(option);
|
||
}
|
||
row.append(words, choice);
|
||
body.append(row);
|
||
}
|
||
const row = el('div', 'anomalous-ai-gates-row');
|
||
const words = el('div', 'anomalous-ai-gates-words');
|
||
words.append(el('div', 'anomalous-ai-gates-name', t('aiGate_canvasTab')), el('div', 'anomalous-ai-gates-tools', t('aiGateWhy_canvasTab')));
|
||
const choice = el('div', 'anomalous-ai-gates-choice');
|
||
choice.setAttribute('role', 'radiogroup');
|
||
for (const mode of ['new', 'current']) {
|
||
const option = button(`anomalous-ai-gates-option${canvasTab === mode ? ' is-on' : ''}`, t(`aiCanvasTab_${mode}`), async () => {
|
||
try {
|
||
canvasTab = (await post('/anomalous/ai_gates', { canvas_tab: mode })).canvas_tab;
|
||
} catch (error) {
|
||
await anomalousAlert(error.message);
|
||
return;
|
||
}
|
||
choice.querySelectorAll('button').forEach((item, at) => item.classList.toggle('is-on', ['new', 'current'][at] === canvasTab));
|
||
});
|
||
option.setAttribute('role', 'radio');
|
||
choice.append(option);
|
||
}
|
||
row.append(words, choice);
|
||
body.append(row, el('p', 'anomalous-ai-gates-note', t('aiGatesNote')));
|
||
aiWindow(t('aiGates'), body);
|
||
}
|