Files
DemonGatanjieu-Anomalous_Mo…/web/modules/browser_entry.js
T
DemonGatanjieuandClaude Opus 5.5 58b11deae0 Quick search on the canvas (Alt+B)
A bar over the canvas finds combos, models and saved prompts as you type
(GET /anomalous/quick_index lists the models in a few words) and puts the chosen one
where the pointer was. Also in the empty canvas's right-click menu, now registered.
Saved model paths in a linked model folder get their real place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:34:41 +08:00

451 lines
19 KiB
JavaScript

import { app } from '../../../scripts/app.js';
import { AnomalousBrowser } from './browser.js';
import { watchMcpActions } from './mcp_bridge.js';
import { canvasMenuItems, nodeMenuItems } from './canvas_menu.js';
import { openQuickSearch } from './ui_quick_search.js';
import {
clampFloatingTriggerPosition,
clearSavedTriggerPosition,
DEFAULT_MIN_SAFE_X,
loadSavedTriggerPosition,
normalizeEntryMode,
normalizeFloatingTriggerSize,
normalizeFloatingTriggerStyle,
removeLegacyTriggerPosition,
saveTriggerPosition
} from './entry_controls.js';
import {
createShortcutSettingControl,
DEFAULT_BROWSER_SHORTCUT,
} from './shortcut_controls.js';
export const ENTRY_MODE_SETTING_ID = 'Anomalous.ModelBrowser.EntryMode';
export const SHORTCUT_SETTING_ID = 'Anomalous.ModelBrowser.Shortcut';
export const FLOATING_TRIGGER_SIZE_SETTING_ID = 'Anomalous.ModelBrowser.FloatingTriggerSize';
export const FLOATING_TRIGGER_STYLE_SETTING_ID = 'Anomalous.ModelBrowser.FloatingTriggerStyle';
export const OPEN_BROWSER_COMMAND_ID = 'Anomalous.ModelBrowser.Open';
// The canvas's quick search bar (ui_quick_search.js); Alt+B unless rebound in ComfyUI's keybindings.
export const QUICK_SEARCH_COMMAND_ID = 'Anomalous.ModelBrowser.QuickSearch';
export const RESET_TRIGGER_POSITION_COMMAND_ID = 'Anomalous.ModelBrowser.ResetFloatingTriggerPosition';
export function createBrowserEntry({ translate, getCurrentLanguage }) {
let entryMode = 'floating';
let floatingTriggerSize = 'medium';
let floatingTriggerStyle = 'icon';
let browserInstance = null;
let triggerButton = null;
let triggerBoundsUpdater = null;
const t = translate;
function getSettingTranslationPatches() {
const category = t('mainInterfaceCategory');
return {
[FLOATING_TRIGGER_STYLE_SETTING_ID]: {
name: t('mainFloatingTriggerStyleSetting'),
category: ['Anomalous Model Browser', category, 'floating-trigger-style'],
tooltip: t('mainFloatingTriggerStyleTooltip'),
options: [
{ value: 'icon', text: t('mainFloatingTriggerStyleIcon') },
{ value: 'pill', text: t('mainFloatingTriggerStylePill') }
]
},
[FLOATING_TRIGGER_SIZE_SETTING_ID]: {
name: t('mainFloatingTriggerSizeSetting'),
category: ['Anomalous Model Browser', category, 'floating-trigger-size'],
tooltip: t('mainFloatingTriggerSizeTooltip'),
options: [
{ value: 'small', text: t('mainFloatingTriggerSizeSmall') },
{ value: 'medium', text: t('mainFloatingTriggerSizeMedium') },
{ value: 'large', text: t('mainFloatingTriggerSizeLarge') }
]
},
[SHORTCUT_SETTING_ID]: {
name: t('mainShortcutSetting'),
category: ['Anomalous Model Browser', category, 'shortcut'],
tooltip: t('mainShortcutTooltip')
},
[ENTRY_MODE_SETTING_ID]: {
name: t('mainEntryModeSetting'),
category: ['Anomalous Model Browser', category, 'entry-mode'],
tooltip: t('mainEntryModeTooltip'),
options: [
{ value: 'floating', text: t('mainEntryModeFloating') },
{ value: 'topbar', text: t('mainEntryModeTopbar') },
{ value: 'menu', text: t('mainEntryModeMenu') }
]
}
};
}
function refreshRegisteredSettings() {
const settingsApi = app.extensionManager?.setting;
const registry = settingsApi?.settings?.value || settingsApi?.settings;
if (!registry || typeof registry !== 'object') return;
for (const [id, patch] of Object.entries(getSettingTranslationPatches())) {
if (registry[id]) registry[id] = { ...registry[id], ...patch };
}
}
function syncVisibility() {
document.documentElement.classList.toggle('anomalous-topbar-entry-enabled', entryMode === 'topbar');
document.documentElement.classList.toggle('anomalous-floating-entry-enabled', entryMode === 'floating');
const browserIsOpen = browserInstance?.modal?.classList.contains('visible') === true;
triggerButton?.classList.toggle('anomalous-trigger-hidden', entryMode !== 'floating' || browserIsOpen);
}
function applyPresentation() {
if (!triggerButton) return;
triggerButton.dataset.size = floatingTriggerSize;
triggerButton.dataset.style = floatingTriggerStyle;
const icon = document.createElement('span');
icon.className = 'anomalous-trigger-icon';
icon.setAttribute('aria-hidden', 'true');
icon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:1em;height:1em;display:block;"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
const label = document.createElement('span');
label.className = 'anomalous-trigger-label';
label.textContent = t('mainFloatingTriggerLabel');
triggerButton.replaceChildren(icon, label);
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
if (loadSavedTriggerPosition()) {
requestAnimationFrame(() => triggerBoundsUpdater?.());
}
}
function resetPosition() {
clearSavedTriggerPosition();
if (!triggerButton) return;
triggerButton.style.left = '';
triggerButton.style.top = '';
triggerButton.style.right = '';
triggerButton.style.bottom = '';
}
function ensureBrowser() {
if (browserInstance) return browserInstance;
try {
browserInstance = new AnomalousBrowser();
browserInstance.entryMode = entryMode;
browserInstance.triggerButton = triggerButton;
window.anomalousBrowserInstance = browserInstance;
triggerButton?.classList.remove('anomalous-trigger-error');
if (triggerButton) {
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
}
syncVisibility();
return browserInstance;
} catch (error) {
console.error('[Anomalous Model Browser] UI initialization failed:', error);
triggerButton?.classList.add('anomalous-trigger-error');
if (triggerButton) {
triggerButton.title = getCurrentLanguage() === 'zh' ? t('mainRetryInit') : t('mainRetryInitEn');
triggerButton.setAttribute('aria-label', triggerButton.title);
}
return null;
}
}
// Opened by the user: the selected node may pick the page (browser.show).
function open() {
ensureBrowser()?.show({ followSelection: true });
}
// The shortcut and the Extensions menu entry: open, or close when it is open.
function toggle() {
const browser = ensureBrowser();
if (browser?.modal?.classList.contains('visible')) browser.close();
else browser?.show({ followSelection: true });
}
const translationPatches = getSettingTranslationPatches();
const settings = [
{
id: FLOATING_TRIGGER_STYLE_SETTING_ID,
...translationPatches[FLOATING_TRIGGER_STYLE_SETTING_ID],
type: 'combo',
defaultValue: 'icon',
onChange(value) {
floatingTriggerStyle = normalizeFloatingTriggerStyle(value);
applyPresentation();
}
},
{
id: FLOATING_TRIGGER_SIZE_SETTING_ID,
...translationPatches[FLOATING_TRIGGER_SIZE_SETTING_ID],
type: 'combo',
defaultValue: 'medium',
onChange(value) {
floatingTriggerSize = normalizeFloatingTriggerSize(value);
applyPresentation();
}
},
{
id: SHORTCUT_SETTING_ID,
...translationPatches[SHORTCUT_SETTING_ID],
type: () => createShortcutSettingControl({ app, commandId: OPEN_BROWSER_COMMAND_ID, translate: t }),
defaultValue: '',
telemetry: { trackChanges: false }
},
{
id: ENTRY_MODE_SETTING_ID,
...translationPatches[ENTRY_MODE_SETTING_ID],
type: 'combo',
defaultValue: 'floating',
onChange(value) {
entryMode = normalizeEntryMode(value);
if (browserInstance) browserInstance.entryMode = entryMode;
syncVisibility();
}
}
];
window.addEventListener('anomalous-language-change', () => {
applyPresentation();
refreshRegisteredSettings();
});
/** Size before the button is measurable; matches the size presets in 00-foundation-models.css. */
function estimatedTriggerSize() {
return floatingTriggerSize === 'small' ? 44 : (floatingTriggerSize === 'large' ? 76 : 60);
}
async function setup() {
removeLegacyTriggerPosition();
const cssUrl = '/extensions/Anomalous_Model_Browser/styles.css?v=20261007-theme-2';
if (!document.querySelector('link[href^="/extensions/Anomalous_Model_Browser/styles.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = cssUrl;
document.head.appendChild(link);
}
try {
const registeredSettings = app.extensionManager?.setting;
entryMode = normalizeEntryMode(registeredSettings?.get(ENTRY_MODE_SETTING_ID));
floatingTriggerSize = normalizeFloatingTriggerSize(registeredSettings?.get(FLOATING_TRIGGER_SIZE_SETTING_ID));
floatingTriggerStyle = normalizeFloatingTriggerStyle(registeredSettings?.get(FLOATING_TRIGGER_STYLE_SETTING_ID));
} catch (error) {
console.warn('[Anomalous Model Browser] Unable to read entry preferences:', error);
}
const btn = document.createElement('button');
btn.id = 'anomalous-trigger-btn';
btn.setAttribute('aria-label', 'Anomalous Model Browser');
// Prevent FOUC: Start hidden until presentation & exact coordinates are bound
btn.classList.add('anomalous-trigger-initializing');
triggerButton = btn;
if (browserInstance) browserInstance.triggerButton = btn;
btn.title = t('mainOpenTitle');
// Pre-configure content & presentation BEFORE mounting
applyPresentation();
// Pre-calculate and assign exact target coordinates BEFORE appending to DOM
const savedInitialPos = loadSavedTriggerPosition();
if (savedInitialPos) {
const estSize = estimatedTriggerSize();
const initialClamped = clampFloatingTriggerPosition({
x: savedInitialPos.x,
y: savedInitialPos.y,
width: estSize,
height: estSize,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: DEFAULT_MIN_SAFE_X
});
btn.style.position = 'fixed';
btn.style.left = initialClamped.x + 'px';
btn.style.top = initialClamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
}
let isDragging = false;
let hasMoved = false;
let startPointerX = 0;
let startPointerY = 0;
let startBtnLeft = 0;
let startBtnTop = 0;
let currentDragX = null;
let currentDragY = null;
let activePointerId = null;
const onPointerMove = (event) => {
if (!isDragging) return;
const dx = event.clientX - startPointerX;
const dy = event.clientY - startPointerY;
if (!hasMoved && Math.hypot(dx, dy) >= 4) {
hasMoved = true;
}
if (!hasMoved) return;
event.preventDefault();
const btnW = btn.offsetWidth || 60;
const btnH = btn.offsetHeight || 60;
const clamped = clampFloatingTriggerPosition({
x: startBtnLeft + dx,
y: startBtnTop + dy,
width: btnW,
height: btnH,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: DEFAULT_MIN_SAFE_X
});
currentDragX = clamped.x;
currentDragY = clamped.y;
btn.style.left = clamped.x + 'px';
btn.style.top = clamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
};
const finishDrag = (event) => {
if (!isDragging) return;
isDragging = false;
btn.removeEventListener('pointermove', onPointerMove);
btn.removeEventListener('pointerup', finishDrag);
btn.removeEventListener('pointercancel', finishDrag);
btn.removeEventListener('lostpointercapture', finishDrag);
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', finishDrag);
window.removeEventListener('pointercancel', finishDrag);
if (activePointerId != null) {
try {
if (btn.hasPointerCapture(activePointerId)) {
btn.releasePointerCapture(activePointerId);
}
} catch (_) {}
activePointerId = null;
}
btn.style.transition = '';
if (hasMoved && currentDragX != null && currentDragY != null) {
saveTriggerPosition({ x: currentDragX, y: currentDragY });
} else if (!hasMoved) {
open();
}
};
btn.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
isDragging = true;
hasMoved = false;
startPointerX = event.clientX;
startPointerY = event.clientY;
activePointerId = event.pointerId;
const rect = btn.getBoundingClientRect();
startBtnLeft = rect.left;
startBtnTop = rect.top;
currentDragX = rect.left;
currentDragY = rect.top;
btn.style.transition = 'none';
try {
btn.setPointerCapture(event.pointerId);
} catch (_) {}
btn.addEventListener('pointermove', onPointerMove, { passive: false });
btn.addEventListener('pointerup', finishDrag);
btn.addEventListener('pointercancel', finishDrag);
btn.addEventListener('lostpointercapture', finishDrag);
window.addEventListener('pointermove', onPointerMove, { passive: false });
window.addEventListener('pointerup', finishDrag);
window.addEventListener('pointercancel', finishDrag);
});
const updateBtnBounds = () => {
const saved = loadSavedTriggerPosition();
if (saved) {
const btnW = btn.offsetWidth || estimatedTriggerSize();
const btnH = btn.offsetHeight || btnW;
const clamped = clampFloatingTriggerPosition({
x: saved.x,
y: saved.y,
width: btnW,
height: btnH,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: DEFAULT_MIN_SAFE_X
});
btn.style.left = clamped.x + 'px';
btn.style.top = clamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
} else {
btn.style.left = '';
btn.style.top = '';
btn.style.right = '';
btn.style.bottom = '';
}
};
triggerBoundsUpdater = updateBtnBounds;
window.addEventListener('resize', () => {
updateBtnBounds();
syncVisibility();
});
// Mount pre-positioned and styled element to DOM
document.body.appendChild(btn);
syncVisibility();
ensureBrowser();
// For the Anomalous_TTS node's character menu (its docs/INTERFACE.md §6).
// Canvas actions asked for by AI apps through the MCP endpoint (api/mcp_bridge.py).
watchMcpActions(ensureBrowser);
window.anomalous_open_voice = (character) => {
const browser = ensureBrowser();
if (!browser) return false;
browser.show();
browser.openVoice(String(character || ''));
return true;
};
// Reveal smoothly on first animation frame without any positional jump or flicker
requestAnimationFrame(() => {
updateBtnBounds();
btn.classList.remove('anomalous-trigger-initializing');
});
window.anomalousDragGhostImg = new Image();
window.anomalousDragGhostImg.src = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='76' height='76' x='2' y='2' fill='%23140812' fill-opacity='0.85' rx='16' stroke='%23f59e0b' stroke-width='2'/><text x='40' y='50' font-family='sans-serif' font-size='32' font-weight='bold' fill='%23f59e0b' text-anchor='middle'>W</text></svg>";
}
return {
settings,
actionBarButtons: [{
icon: 'pi pi-box',
label: t('mainTopbarTriggerLabel'),
tooltip: t('mainOpenTitle'),
class: 'anomalous-topbar-entry',
onClick: open
}],
commands: [
{ id: OPEN_BROWSER_COMMAND_ID, label: t('mainOpenTitle'), function: toggle },
{ id: RESET_TRIGGER_POSITION_COMMAND_ID, label: t('mainResetFloatingTriggerPosition'), function: resetPosition },
{ id: QUICK_SEARCH_COMMAND_ID, label: t('quickSearchTitle'), function: () => openQuickSearch(ensureBrowser) },
],
keybindings: [
{ combo: DEFAULT_BROWSER_SHORTCUT, commandId: OPEN_BROWSER_COMMAND_ID },
{ combo: { key: 'b', alt: true }, commandId: QUICK_SEARCH_COMMAND_ID },
],
menuCommands: [{
path: ['Extensions', 'Anomalous Model Browser'],
commands: [OPEN_BROWSER_COMMAND_ID, QUICK_SEARCH_COMMAND_ID, RESET_TRIGGER_POSITION_COMMAND_ID]
}],
setup,
// The canvas's right-click menus: AMB's entries as Settings -> Canvas sets them (canvas_menu.js).
getNodeMenuItems: node => nodeMenuItems(node, ensureBrowser),
getCanvasMenuItems: () => canvasMenuItems(ensureBrowser),
open,
ensureBrowser,
resetPosition,
syncVisibility
};
}