Privacy mode asked to be believed: nothing showed the user what had gone out, and nothing said that it filters Anomalous's answers only. - ai_sent.py keeps each call of a private task as it went: the arguments the AI wrote (tokens) and the answer's text after ai_privacy.guard. A line per call beside the task, the newest 300, removed with the task. - The task's menu opens "What the AI was sent" (ui_ai_sent.js): the calls, and a box that looks for a word in all of them. - ai_callers.reads_disk names the AI apps that read files and run commands by themselves. A private task run by one says under its header that such an app can open the output folder and the task records by itself; the switch's hints and the guide say the same. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
70 lines
3.1 KiB
JavaScript
70 lines
3.1 KiB
JavaScript
/**
|
|
* What a private task's AI was sent (api/ai_sent.py): each call with what the AI wrote (its
|
|
* arguments) and the text Anomalous answered, as it left. Opened from the task's menu and read
|
|
* then; the box above the list looks for a word in all of it, so the user sees for themselves
|
|
* whether a name or a prompt went out. A call's texts are drawn when it is opened.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { timeLabel } from './activity_log.js';
|
|
import { aiWindow } from './ui_ai_connect.js';
|
|
|
|
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 callRow(call, open) {
|
|
const row = el('details', `anomalous-ai-sent-call${call.error ? ' is-error' : ''}`);
|
|
const head = el('summary', 'anomalous-ai-sent-head');
|
|
head.append(el('span', 'anomalous-ai-sent-time', timeLabel(call.at)), el('code', '', call.tool),
|
|
el('span', 'anomalous-ai-sent-size', t('aiSentSize', { n: (call.text || '').length })));
|
|
row.append(head);
|
|
const fill = () => {
|
|
if (row.childElementCount > 1) return;
|
|
row.append(el('div', 'anomalous-ai-sent-label', t('aiSentIn')), el('pre', 'anomalous-ai-sent-text', call.arguments),
|
|
el('div', 'anomalous-ai-sent-label', t('aiSentOut')), el('pre', 'anomalous-ai-sent-text', call.text));
|
|
};
|
|
row.addEventListener('toggle', fill);
|
|
if (open) {
|
|
fill();
|
|
row.open = true;
|
|
}
|
|
return row;
|
|
}
|
|
|
|
/** The window for `task`. */
|
|
export async function openSent(task) {
|
|
const body = el('div', 'anomalous-ai-sent');
|
|
body.append(el('p', 'anomalous-ai-gates-intro', t('aiSentIntro')));
|
|
aiWindow(t('aiSent'), body);
|
|
let calls = [];
|
|
try {
|
|
calls = (await (await fetch(`/anomalous/ai_tasks/${encodeURIComponent(task.id)}/sent`)).json()).calls || [];
|
|
} catch (_) { /* drawn as nothing kept */ }
|
|
if (!calls.length) {
|
|
body.append(el('p', 'anomalous-ai-sent-none', t('aiSentEmpty')));
|
|
return;
|
|
}
|
|
calls.reverse(); // the newest first
|
|
const find = el('input', 'anomalous-ai-sent-find');
|
|
find.type = 'search';
|
|
find.placeholder = t('aiSentFind');
|
|
find.addEventListener('keydown', event => event.stopPropagation());
|
|
const count = el('div', 'anomalous-ai-sent-count');
|
|
const list = el('div', 'anomalous-ai-sent-list');
|
|
const draw = () => {
|
|
const word = find.value.trim().toLowerCase();
|
|
const found = word ? calls.filter(call => `${call.tool}\n${call.arguments}\n${call.text}`.toLowerCase().includes(word)) : calls;
|
|
count.textContent = !word ? t('aiSentCount', { n: calls.length }) : found.length ? t('aiSentFound', { n: found.length, all: calls.length })
|
|
: t('aiSentNotFound', { all: calls.length });
|
|
count.classList.toggle('is-clear', Boolean(word) && !found.length);
|
|
list.replaceChildren(...found.map(call => callRow(call, Boolean(word) && found.length <= 5)));
|
|
};
|
|
find.addEventListener('input', draw);
|
|
body.append(find, count, list);
|
|
draw();
|
|
}
|