Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_prompt_box.js
T
DemonGatanjieuandClaude Opus 5.5 eceb51b6a1 Seal a private task's prompts and picture names as tokens, with a prompt box
ai_sealed keeps a private task's prompt text and picture file names behind tokens ([P1],
[img1], task["sealed"]): mcp_server puts the texts back into a call's arguments for the tool
only, and ai_privacy.guard has every result masked on its way out (a node's STRING inputs
by ComfyUI's node list, prompt fields, picture names, any sealed text, also in the text
content). The user's own canvas changes are told sealed. A "prompts" panel block is the
user's prompt box (ui_ai_prompt_box.js): prompts, a negative one and reference pictures from
the gallery or dropped in, answered only from Anomalous, the AI getting tokens; above it the
prompt_pose skill's words for the canvas's model. A new private-only skill, privacy, tells
the AI how to work, and begin_task points a private task at it. Tests plant canary prompts
and picture names in every tool's result and check none gets out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:56:37 +08:00

149 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The prompt box (a "prompts" block of the AI's panel, api/ai_panels.py), for a private task: the
* user writes the prompts here, and a negative one and reference pictures (from the gallery, or
* dropped in) when the AI asks for them. The texts stay in Anomalous; the AI gets tokens for them
* (api/ai_sealed.py). Above the fields, what suits the canvas's model (the prompt_pose skill's words,
* the step's prompt_advice). Answered, it shows what the user wrote beside the token the AI has.
* ui_ai_panel.js draws it and hands it its helpers (sending, the question's head, the answer's note).
*/
import { resolveLocale, translate as t } from './locales.js';
import { galleryPicker, thumbUrl } from './ui_ai_gallery_pick.js';
import { takePictures } from './ui_ai_drop.js';
const MAX_PROMPTS = 8;
const MAX_PICTURES = 4;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = String(text);
return node;
}
function button(className, label, onClick) {
const node = el('button', className, label);
node.type = 'button';
node.onclick = onClick;
return node;
}
const words = value => (resolveLocale() === 'zh' ? value?.zh : value?.en) || value?.en || value?.zh || '';
/** A picture by its path in the output folder ("subfolder/name.png"), as a small tile. */
function tile(path, onRemove) {
const at = path.lastIndexOf('/');
const image = { filename: path.slice(at + 1), subfolder: at > 0 ? path.slice(0, at) : '' };
const node = el('span', 'anomalous-ai-prompt-pic');
const img = el('img');
img.alt = image.filename;
img.title = path;
img.src = thumbUrl(image);
node.append(img);
if (onRemove) node.append(button('anomalous-ai-prompt-pic-remove', '×', onRemove));
return node;
}
/** What the user handed in: each token the AI has, with the text it stands for (the task's `sealed`). */
function handedIn(answer, sealed) {
const box = el('div', 'anomalous-ai-prompt-given');
const line = (token, label) => {
const row = el('div', 'anomalous-ai-prompt-given-row');
row.append(el('code', 'anomalous-ai-prompt-token', token), el('span', 'anomalous-ai-prompt-label', label),
el('span', 'anomalous-ai-prompt-text', sealed?.[token] || ''));
box.append(row);
};
(answer.prompts || []).forEach((token, at) => line(token, t('aiPromptBoxPrompt', { n: at + 1 })));
if (answer.negative) line(answer.negative, t('aiPromptBoxNegative'));
if (answer.pictures?.length) {
const row = el('div', 'anomalous-ai-prompt-given-row');
row.append(el('span', 'anomalous-ai-prompt-label', t('aiPromptBoxPictures')));
for (const token of answer.pictures) {
const pic = el('span', 'anomalous-ai-prompt-given-pic');
pic.append(el('code', 'anomalous-ai-prompt-token', token), ...(sealed?.[token] ? [tile(sealed[token])] : []));
row.append(pic);
}
box.append(row);
}
return box;
}
/** The box for `block` of the panel; `ctx` as ui_ai_panel.js gives its blocks, plus `advice` and `sealed`. */
export function promptBox(block, ctx, { sendAnswer, askHead, skipButton, answerNote, draftField }) {
const box = el('div', 'anomalous-ai-panel-ask anomalous-ai-prompt-box');
const answer = ctx.answers[block.id];
const note = el('div', 'anomalous-ai-panel-error');
box.append(askHead(block.question || t('aiPromptBoxAsk'), !answer && !ctx.closed && skipButton(ctx, block, note), ctx));
if (ctx.advice) box.append(el('div', 'anomalous-ai-prompt-advice', words(ctx.advice)));
if (answer) {
if (!answer.skipped) box.append(handedIn(answer, ctx.sealed), el('div', 'anomalous-ai-prompt-private', t('aiPromptBoxPrivate')));
box.append(answerNote(answer));
return box;
}
const fields = el('div', 'anomalous-ai-prompt-fields');
const field = (key, placeholder, rows) => {
const area = el('textarea', 'anomalous-ai-prompt-field');
area.rows = rows;
area.maxLength = 4000;
area.placeholder = placeholder;
area.disabled = ctx.closed;
draftField(area, ctx.taskId, ctx.stepId, `${block.id}#${key}`);
area.addEventListener('keydown', event => event.stopPropagation()); // typing here is not the browser's shortcuts
return area;
};
const prompts = [];
const addPrompt = () => {
const area = field(prompts.length, t('aiPromptBoxPrompt', { n: prompts.length + 1 }), 3);
prompts.push(area);
fields.append(area);
more.hidden = prompts.length >= MAX_PROMPTS;
};
const more = button('anomalous-ai-prompt-more', t('aiPromptBoxMore'), () => {
addPrompt();
prompts.at(-1).focus();
});
for (let at = 0; at < Math.max(1, Math.min(MAX_PROMPTS, block.count || 1)); at++) addPrompt();
box.append(fields, more);
const negative = block.negative ? field('negative', t('aiPromptBoxNegative'), 2) : null;
if (negative) box.append(negative);
const pictures = new Map(); // path -> {filename, subfolder}
if (block.pictures) {
const row = el('div', 'anomalous-ai-prompt-pics');
const tray = el('div', 'anomalous-ai-prompt-tray');
const room = () => MAX_PICTURES - pictures.size;
const add = (item) => {
const image = item.image || item;
const path = [image.subfolder, image.filename].filter(Boolean).join('/');
if (pictures.has(path) || !room()) return;
pictures.set(path, { filename: image.filename, subfolder: image.subfolder || '' });
const shown = tile(path, () => { pictures.delete(path); shown.remove(); });
tray.append(shown);
};
const pick = button('anomalous-ai-prompt-pick', t('aiPromptBoxPick'), () => picker.toggle());
const picker = galleryPicker(box, room, items => items.forEach(add), pick,
{ hint: 'aiPromptBoxPicsHint', full: 'aiPromptBoxPicsFull', add: 'aiPromptBoxPicsAdd' });
takePictures(box, prompts[0], { add, room, say: (text) => { note.textContent = text; } });
row.append(el('span', 'anomalous-ai-prompt-label', t('aiPromptBoxPictures')), tray, pick, el('span', 'anomalous-ai-prompt-hint', t('aiPromptBoxDrop')));
box.append(row);
}
const send = button('anomalous-ai-panel-send', t('aiPromptBoxSend'), async () => {
const texts = prompts.map(area => area.value.trim()).filter(Boolean);
if (!texts.length) {
note.textContent = t('aiPromptBoxEmpty');
return;
}
send.disabled = true;
try {
await sendAnswer(ctx.taskId, { step: ctx.stepId, block: block.id, prompts: texts, negative: negative?.value.trim() || '', pictures: [...pictures.values()] });
ctx.changed();
} catch (error) {
note.textContent = error.message;
send.disabled = false;
}
});
send.disabled = ctx.closed;
box.append(el('div', 'anomalous-ai-prompt-private', t('aiPromptBoxPrivate')), send, note);
return box;
}