The user took an option for something to select and then pressed "skip" to confirm; boxes shown open read as fields to fill. - Questions (stage panels, the strip over the message box): options are buttons with an arrow under "One press sends it to the AI" and show the pressed one at once; "Let the AI decide" moves to the question's top right (the panel title for ask_user); a line of one's own is folded as "None of these, I'll say it…". The Gates card folds its reason the same. - A waiting task is one page: the sentence for the AI leads and sticks under the task header while the page scrolls (the workbench measures the header); "Say what to do" and the gallery are optional folds that say how many pictures are in. The sentence has the AI do what the task says, asking only when it says nothing. The Get started / Put ready tabs and their words are gone. - Gates window texts fixed (canvas covers building, lines are over the message box, "Ask me" named as on the button); run_workflow waits up to 55 s by default, so a quick run comes back with its pictures. - The UI map counts words the backend names (label_key) as used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
6.3 KiB
JavaScript
142 lines
6.3 KiB
JavaScript
/**
|
||
* How to connect an AI app to Anomalous (the AI workbench's "Connect an AI", its empty page and a
|
||
* task waiting for an AI): the address, a sentence to paste to the AI so it sets itself up (and
|
||
* starts on the task, when one waits), the way for each common app, and rules for the AI app to
|
||
* keep (use Anomalous's tools, never edit pictures with code, say what it did outside). Only this computer can
|
||
* connect; the address is this page's own. Connecting (connectHint) is a card that opens the guide
|
||
* in place, under a task waiting for an AI (ui_ai_ready.js) and on the empty page.
|
||
*/
|
||
|
||
import { translate as t } from './locales.js';
|
||
import { copyText } from './ui_dialog.js';
|
||
import { iconButton } from './ui_ai_icons.js';
|
||
|
||
const ART = {
|
||
waiting: new URL('../assets/ai_waiting.webp', import.meta.url).href,
|
||
connect: new URL('../assets/ai_connect.webp', import.meta.url).href,
|
||
};
|
||
|
||
export function art(name, className) {
|
||
const img = el('img', className);
|
||
img.src = ART[name];
|
||
img.alt = '';
|
||
img.decoding = 'async';
|
||
img.setAttribute('aria-hidden', 'true');
|
||
return img;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
export const mcpAddress = () => `${location.origin}/anomalous/mcp`;
|
||
|
||
/** A line to copy: the text and a Copy button that says when it is done. */
|
||
export function copyLine(text, { code = true, label = t('aiCopyAddress') } = {}) {
|
||
const line = el('div', `anomalous-ai-copy${code ? ' is-code' : ''}`);
|
||
const body = el(code ? 'code' : 'span', 'anomalous-ai-copy-text', text);
|
||
const copy = iconButton('anomalous-ai-copy-button', 'copy', label, async () => {
|
||
await copyText(text);
|
||
copy.classList.add('is-done');
|
||
copy.lastChild.textContent = t('aiCopied');
|
||
setTimeout(() => {
|
||
copy.classList.remove('is-done');
|
||
copy.lastChild.textContent = label;
|
||
}, 1800);
|
||
});
|
||
line.append(body, copy);
|
||
return line;
|
||
}
|
||
|
||
/** The sentence for the AI: add the server, then (with `title`) start on that task. */
|
||
export function sentenceForAi(title = '') {
|
||
const add = t('aiConnectSentence', { url: mcpAddress() });
|
||
return title ? `${add}${t('aiConnectSentenceTask', { task: title })}` : add;
|
||
}
|
||
|
||
/** Everything about connecting, as one block. `title`: the task waiting for an AI, if any. */
|
||
export function connectGuide(title = '') {
|
||
const url = mcpAddress();
|
||
const box = el('div', 'anomalous-ai-connect');
|
||
box.append(el('div', 'anomalous-ai-connect-step', t('aiConnectStepAddress')), copyLine(url));
|
||
box.append(el('div', 'anomalous-ai-connect-step', t(title ? 'aiConnectStepSayTask' : 'aiConnectStepSay')),
|
||
copyLine(sentenceForAi(title), { code: false, label: t('aiConnectCopySentence') }));
|
||
const apps = el('details', 'anomalous-ai-connect-apps');
|
||
apps.append(el('summary', '', t('aiConnectByApp')));
|
||
const ways = [
|
||
['Claude Code', `claude mcp add --transport http anomalous ${url}`],
|
||
['Gemini CLI', `gemini mcp add --transport http anomalous ${url}`],
|
||
['Cursor (~/.cursor/mcp.json)', `{ "mcpServers": { "anomalous": { "url": "${url}" } } }`],
|
||
['VS Code (.vscode/mcp.json)', `{ "servers": { "anomalous": { "type": "http", "url": "${url}" } } }`],
|
||
];
|
||
for (const [name, text] of ways) apps.append(el('div', 'anomalous-ai-connect-app', name), copyLine(text));
|
||
apps.append(el('div', 'anomalous-ai-connect-app', 'Cherry Studio'), el('div', 'anomalous-ai-connect-note', t('aiConnectCherry', { url })));
|
||
const rules = copyLine(t('aiConnectRules'), { code: false, label: t('aiConnectCopyRules') });
|
||
rules.classList.add('is-rules');
|
||
box.append(apps, el('div', 'anomalous-ai-connect-step', t('aiConnectStepRules')), rules,
|
||
el('div', 'anomalous-ai-connect-note', t('aiConnectLocal')));
|
||
return box;
|
||
}
|
||
|
||
/** The card for an AI not connected yet: connecting is done once; its button opens the guide in place. */
|
||
export function connectHint(title = '') {
|
||
const card = el('div', 'anomalous-ai-hint');
|
||
const words = el('div', 'anomalous-ai-hint-words');
|
||
words.append(el('div', 'anomalous-ai-hint-title', t('aiHintTitle')), el('p', 'anomalous-ai-hint-body', t('aiHintBody')));
|
||
const guide = el('div', 'anomalous-ai-hint-guide');
|
||
guide.hidden = true;
|
||
const open = iconButton('anomalous-ai-hint-open', 'plug', t('aiHintOpen'), () => {
|
||
if (!guide.children.length) guide.append(connectGuide(title));
|
||
guide.hidden = !guide.hidden;
|
||
open.setAttribute('aria-expanded', String(!guide.hidden));
|
||
open.lastChild.textContent = t(guide.hidden ? 'aiHintOpen' : 'aiHintClose');
|
||
});
|
||
open.setAttribute('aria-expanded', 'false');
|
||
words.append(open);
|
||
const row = el('div', 'anomalous-ai-hint-row');
|
||
row.append(art('connect', 'anomalous-ai-hint-art'), words);
|
||
card.append(row, guide);
|
||
return card;
|
||
}
|
||
|
||
/** A window over the page with `title` and `body` (Esc, ×, or the backdrop closes it); returns close. */
|
||
export function aiWindow(title, body) {
|
||
const overlay = el('div', 'anomalous-ai-connect-overlay');
|
||
const dialog = el('div', 'anomalous-ai-connect-dialog');
|
||
dialog.setAttribute('role', 'dialog');
|
||
dialog.setAttribute('aria-modal', 'true');
|
||
const close = () => {
|
||
overlay.remove();
|
||
window.removeEventListener('keydown', onKey, true);
|
||
};
|
||
const onKey = (event) => {
|
||
if (event.key !== 'Escape') return;
|
||
event.stopPropagation();
|
||
close();
|
||
};
|
||
const head = el('div', 'anomalous-ai-connect-head');
|
||
head.append(el('div', 'anomalous-ai-connect-title', title), button('anomalous-ai-connect-close', '×', close, t('close')));
|
||
dialog.append(head, body);
|
||
overlay.append(dialog);
|
||
overlay.addEventListener('click', (event) => { if (event.target === overlay) close(); });
|
||
window.addEventListener('keydown', onKey, true);
|
||
document.body.append(overlay);
|
||
return close;
|
||
}
|
||
|
||
/** The guide in a window over the page. */
|
||
export function openConnectGuide() {
|
||
aiWindow(t('aiConnect'), connectGuide());
|
||
}
|