Every page in both languages, all themes and at 480: - Give the window its own text colour: text with none took ComfyUI's, white on the light theme (Tidy headings and file names, the site chip). - Keep a task's link chip in its row, its x in reach, and colour the next-task chip. - Let choices in Settings wrap, the Models search shrink, and a workflow's preset row go to two lines. - Add --amb-on-link for text on the accent: pale in the dark themes, so white was hard to read. - Lists and colons follow the language (listSeparator, wordColon); tests/words_in_language.mjs keeps it so. - Right plurals in English, "AI" on the rail, one line left in Chinese translated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
7.7 KiB
JavaScript
157 lines
7.7 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, a reason for the AI folded until asked for, 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), whether a new
|
|
* task works in a canvas tab of its own (api/ai_task_canvas.py), how long the AI waits for the
|
|
* user's answer and what happens when nobody answers, and whether the AI waits for a run or the user tells it when the run is done
|
|
* (ui_ai_call.js).
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { anomalousAlert } from './ui_dialog.js';
|
|
import { aiWindow } from './ui_ai_connect.js';
|
|
import { icon } from './ui_ai_icons.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;
|
|
}
|
|
|
|
/** The user's answer to a step waiting at a gate: {decision: allow | deny, text?, remember?}. */
|
|
export function decideGate(taskId, stepId, answer) {
|
|
return post(`/anomalous/ai_tasks/${encodeURIComponent(taskId)}/gate`, { step: stepId, ...answer });
|
|
}
|
|
|
|
/** 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 ? `${t('wordColon')}${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.hidden = true; // a reason is optional: the line opens from "say how to change it"
|
|
reason.addEventListener('keydown', event => event.stopPropagation());
|
|
const why = button('anomalous-ai-panel-own', '', () => {
|
|
reason.hidden = false;
|
|
why.remove();
|
|
reason.focus();
|
|
});
|
|
why.append(icon('pencil', 13), el('span', '', t('aiGateSayWhy')));
|
|
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 decideGate(taskId, 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')), why, remember);
|
|
card.append(reason, actions);
|
|
if (asked.timed_out) card.append(el('div', 'anomalous-ai-gate-note', t('aiGateLate')));
|
|
return card;
|
|
}
|
|
|
|
/** A row of the Gates window: a setting, its words and its choices; `save(choice)` posts it and gives what is kept. */
|
|
function choiceRow(key, choices, current, labelOf, save) {
|
|
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_${key}`)), el('div', 'anomalous-ai-gates-tools', t(`aiGateWhy_${key}`)));
|
|
const choice = el('div', 'anomalous-ai-gates-choice');
|
|
choice.setAttribute('role', 'radiogroup');
|
|
let kept = current;
|
|
const draw = () => choice.querySelectorAll('button').forEach((item, at) => item.classList.toggle('is-on', choices[at] === kept));
|
|
for (const value of choices) {
|
|
const option = button('anomalous-ai-gates-option', labelOf(value), async () => {
|
|
try {
|
|
kept = await save(value);
|
|
} catch (error) {
|
|
await anomalousAlert(error.message);
|
|
return;
|
|
}
|
|
draw();
|
|
});
|
|
option.setAttribute('role', 'radio');
|
|
choice.append(option);
|
|
}
|
|
draw();
|
|
row.append(words, choice);
|
|
return row;
|
|
}
|
|
|
|
/** The Gates window: each kind of action the AI may do (goes ahead, asks or never), where it works, which tools it gets. */
|
|
export async function openGates() {
|
|
const body = el('div', 'anomalous-ai-gates');
|
|
body.append(el('p', 'anomalous-ai-gates-intro', t('aiGatesIntro')));
|
|
let known = {};
|
|
try {
|
|
known = await (await fetch('/anomalous/ai_gates')).json();
|
|
} catch (_) { /* drawn with nothing chosen; a change still saves */ }
|
|
const save = async change => post('/anomalous/ai_gates', change);
|
|
for (const gate of GATES) {
|
|
body.append(choiceRow(gate, POLICIES, known.policies?.[gate], policy => t(`aiPolicy_${policy}`),
|
|
async policy => (await save({ gate, policy })).policies[gate]));
|
|
}
|
|
body.append(choiceRow('canvasTab', ['new', 'current'], known.canvas_tab || 'new', mode => t(`aiCanvasTab_${mode}`),
|
|
async mode => (await save({ canvas_tab: mode })).canvas_tab),
|
|
choiceRow('privacy', [false, true], known.privacy === true, on => t(`aiPrivacyDefault_${on}`),
|
|
async on => (await save({ privacy: on })).privacy),
|
|
choiceRow('answerWait', [60, 180, 600], known.answer_wait || 180, seconds => t(`aiAnswerWait_${seconds}`),
|
|
async seconds => (await save({ answer_wait: seconds })).answer_wait),
|
|
choiceRow('noAnswer', ['stop', 'recommended'], known.no_answer || 'stop', mode => t(`aiNoAnswer_${mode}`),
|
|
async mode => (await save({ no_answer: mode })).no_answer),
|
|
choiceRow('runs', ['call', 'wait'], known.runs || 'call', mode => t(`aiRunWait_${mode}`),
|
|
async mode => (await save({ runs: mode })).runs),
|
|
choiceRow('nodes', ['any', 'ways'], known.nodes || 'any', mode => t(`aiNodeRule_${mode}`),
|
|
async mode => (await save({ nodes: mode })).nodes),
|
|
choiceRow('route', ['allow', 'ask'], known.routes || 'allow', mode => t(`aiRouteRule_${mode}`),
|
|
async mode => (await save({ routes: mode })).routes),
|
|
el('p', 'anomalous-ai-gates-note', t('aiGatesNote')));
|
|
aiWindow(t('aiGates'), body);
|
|
}
|