Files
DemonGatanjieuandClaude Opus 5.5 342946141e Make the AI's questions and the waiting page say what to press
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>
2026-10-26 03:54:20 +08:00

142 lines
6.3 KiB
JavaScript
Raw Permalink 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.
/**
* 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());
}