Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_connect.js
T
DemonGatanjieuandClaude Opus 5.5 cd9751ef14 AI workbench: the flow over a task, and gates that wait for your OK
- api/ai_gates.py: changing the canvas, using the GPU, downloading and scanning online can each
  go ahead, ask first or never (downloads ask by default; anomalous/ai_gates.json). A call that
  asks waits on its step (state "asking", what it would do listed) up to 50 s for Allow or No
  (with the user's reason for the AI); unanswered, the AI is told to wait and call again, the
  later answer reaches it as a message, and the same call after an Allow goes ahead.
- ui_ai_gate.js: the card under a waiting step and the Gates window; a toast while elsewhere.
- ui_ai_flow.js + ai_task_view.flowStages: the task's stages in order, each done, running,
  waiting or failed; a stage leads to its first step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:48:43 +08:00

97 lines
4.2 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.
/**
* 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), and the way for each common app. Only this computer can
* connect; the address is this page's own.
*/
import { translate as t } from './locales.js';
import { copyText } from './ui_dialog.js';
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. */
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 = button('anomalous-ai-copy-button', label, async () => {
await copyText(text);
copy.textContent = t('aiCopied');
setTimeout(() => { copy.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 })));
box.append(apps, el('div', 'anomalous-ai-connect-note', t('aiConnectLocal')));
return box;
}
/** 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());
}