Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_toolbox.js
T
DemonGatanjieuandClaude Opus 5.5 46facb9115 feat(shell): icon rail, per-page list column and a home page
The browser now navigates from one narrow icon rail on the left: Home, Models,
Gallery, Workflows, Materials, then Voices and Audio, then scan, Model Doctor,
Node Assistant and the toolbox, with settings at the bottom. It replaces the
header tabs, the image/audio toggle and the tool bar under the folder list, so
the list column holds only a list and can close without hiding a command.

- ui_shell_rail.js builds the rail; ui_shell_nav.js owns goTo(page), which page
  has a list and whether it is open (remembered per page; below 760 px it starts
  closed and overlays), the page title in the header and the page reopened next
  time. Clicking the current page's icon toggles its list (from a model's detail
  it returns to the grid).
- ui_home.js: task cards and first steps; the first open lands there.
- Window drag/resize moved to ui_shell_frame.js and scan polling to
  ui_scan_watch.js; ui_sidebar.js goes from 864 to 464 lines.
- Workspaces (recipes, notes, materials) keep the rail visible; the list steps
  aside while one is open.
- Removed what the rail replaced: sidebar_actions.js (bottom-bar hover labels and
  tooltip bubble), the domain toggle button, their CSS and unused text keys.
- Spotlight tour: the rail step replaces the header-tabs step, cards open to the
  right of rail items, and the audio tour points at the rail's Audio entry.
- Decision record AD-017; architecture map, frontend and update-guide docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:08:16 +08:00

283 lines
11 KiB
JavaScript

/**
* Toolbox catalog, fixed shortcut actions, and tool dispatch.
*/
import { app } from "../../../scripts/app.js";
import { translate } from './locales.js';
import { openModelSourcesModal } from './ui_model_sources.js';
import { CATALOG_TOOLS, getToolDefinition } from './tool_registry.js';
import { setScanButtonState } from './ui_scan_wizard.js';
const t = (key, params) => translate(key, params);
export function createToolbox(owner, {
container,
toolboxIcon,
isScanning,
getSettingsButton,
onBeforeOpen,
}) {
const toolboxBtn = document.createElement('button');
toolboxBtn.id = 'anomalous-toolbox-btn';
toolboxBtn.className = 'anomalous-tooltip-target';
toolboxBtn.removeAttribute('title');
toolboxBtn.setAttribute('aria-label', t('sidebarToolbox'));
toolboxBtn.setAttribute('data-tooltip', t('sidebarToolbox'));
toolboxBtn.setAttribute('data-tooltip-pos', 'right');
toolboxBtn.innerHTML = toolboxIcon;
const toolboxModal = document.createElement('div');
toolboxModal.id = 'anomalous-toolbox-modal';
toolboxModal.style.display = 'none';
const closeToolbox = (e) => {
if (toolboxModal.style.display !== 'none' && !toolboxModal.contains(e.target) && !toolboxBtn.contains(e.target)) {
toolboxModal.style.display = 'none';
toolboxBtn.classList.remove('is-active');
document.removeEventListener('mousedown', closeToolbox);
}
};
toolboxBtn.onclick = (e) => {
e.stopPropagation();
onBeforeOpen();
if (toolboxModal.style.display === 'none') {
toolboxModal.style.display = 'flex';
toolboxBtn.classList.add('is-active');
setTimeout(() => document.addEventListener('mousedown', closeToolbox), 10);
} else {
toolboxModal.style.display = 'none';
toolboxBtn.classList.remove('is-active');
document.removeEventListener('mousedown', closeToolbox);
}
};
const executeToolAction = async (toolId) => {
switch (toolId) {
case 'scan':
owner.openScanWizard({ isGlobal: true });
break;
case 'doctor':
owner.enterToolPage?.('doctor');
owner.hideAllPanels();
owner.doctorPanel.style.display = 'flex';
if (!owner.doctorPanelInitialized) {
owner.initDoctorPanel();
}
if (window.anomalous_reload_hashes) await window.anomalous_reload_hashes();
if (window.anomalous_resolve_all_missing_nodes) {
await window.anomalous_resolve_all_missing_nodes(true, false);
}
owner.renderGlobalDashboard();
break;
case 'assistant':
owner.enterToolPage?.('assistant');
owner.hideAllPanels();
owner.assistantPanel.style.display = 'flex';
if (!owner.assistantPanelInitialized) {
owner.initAssistantPanel();
}
if (Object.keys(app.canvas?.selected_nodes || {}).length > 0) {
const firstSelected = Object.values(app.canvas.selected_nodes)[0];
owner.diagnoseNode(firstSelected);
} else {
owner.diagnoseNode(null);
}
break;
case 'materials':
owner.goTo('materials');
break;
case 'workflow-transfer':
if (window.AMB_WorkflowShare && typeof window.AMB_WorkflowShare.showUnifiedModal === 'function') {
window.AMB_WorkflowShare.showUnifiedModal();
}
break;
case 'prompt-studio':
if (typeof owner.openPromptStudio === 'function') {
owner.openPromptStudio();
} else if (typeof owner.openMaterialLibrary === 'function') {
owner.openMaterialLibrary();
}
break;
case 'prompt-translator':
if (typeof owner.openPromptTranslator === 'function') {
owner.openPromptTranslator();
}
break;
case 'model-sources':
openModelSourcesModal('workflow');
break;
case 'prompt-notes':
if (typeof owner.showNotebooks === 'function') {
owner.showNotebooks();
}
break;
default: {
const custom = (owner.customToolboxItems || []).find(it => it.id === toolId);
if (custom && typeof custom.action === 'function') {
custom.action();
}
break;
}
}
};
owner.executeToolAction = executeToolAction;
const renderShortcutActions = () => {
owner.railTools.replaceChildren();
// Materials has its own page on the rail; the toolbox comes last, settings at the bottom.
const defaultLayout = ['scan', 'doctor', 'assistant'];
for (const toolId of defaultLayout) {
const def = getToolDefinition(toolId);
if (!def) continue;
const btn = document.createElement('button');
btn.id = def.domId;
btn.setAttribute('data-tool-id', toolId);
btn.className = 'anomalous-tooltip-target';
btn.removeAttribute('title');
btn.setAttribute('aria-label', t(def.nameKey));
btn.setAttribute('data-tooltip', t(def.nameKey));
btn.setAttribute('data-tooltip-pos', 'right');
if (toolId === 'scan') {
setScanButtonState(btn, isScanning());
} else {
btn.innerHTML = def.icon;
}
btn.onclick = (e) => {
e.stopPropagation();
executeToolAction(toolId);
};
owner.railTools.appendChild(btn);
}
owner.railTools.appendChild(toolboxBtn);
owner.railFoot.replaceChildren(getSettingsButton());
};
owner.renderShortcutActions = renderShortcutActions;
const renderToolboxModal = () => {
toolboxModal.replaceChildren();
const headerRow = document.createElement('div');
headerRow.style.display = 'flex';
headerRow.style.alignItems = 'center';
headerRow.style.justifyContent = 'space-between';
headerRow.style.padding = '1px 2px 3px';
headerRow.style.borderBottom = '1px solid rgba(255, 255, 255, 0.06)';
const titleBox = document.createElement('div');
titleBox.style.display = 'flex';
titleBox.style.alignItems = 'center';
titleBox.style.gap = '5px';
const titleIcon = document.createElement('span');
titleIcon.textContent = '🧰';
titleIcon.style.fontSize = '11px';
const titleText = document.createElement('span');
titleText.textContent = t('toolboxTitle');
titleText.style.fontWeight = '600';
titleText.style.fontSize = '11px';
titleText.style.color = '#fff';
titleBox.append(titleIcon, titleText);
const closeModalBtn = document.createElement('div');
closeModalBtn.innerHTML = '&times;';
closeModalBtn.style.cursor = 'pointer';
closeModalBtn.style.color = '#888';
closeModalBtn.style.fontSize = '14px';
closeModalBtn.style.lineHeight = '1';
closeModalBtn.style.padding = '1px 3px';
closeModalBtn.style.borderRadius = '3px';
closeModalBtn.onmouseover = () => { closeModalBtn.style.color = '#fff'; closeModalBtn.style.background = 'rgba(255,255,255,0.08)'; };
closeModalBtn.onmouseout = () => { closeModalBtn.style.color = '#888'; closeModalBtn.style.background = 'transparent'; };
closeModalBtn.onclick = () => {
toolboxModal.style.display = 'none';
toolboxBtn.classList.remove('is-active');
};
headerRow.appendChild(titleBox);
headerRow.appendChild(closeModalBtn);
toolboxModal.appendChild(headerRow);
const gridContainer = document.createElement('div');
gridContainer.className = 'anomalous-toolbox-grid';
// 已经在左侧图标栏上的工具(扫描、医生、助手、素材、工具箱、设置)不再放进工具箱
const outsideToolIds = new Set(['scan', 'doctor', 'assistant', 'materials', 'toolbox', 'settings']);
const allTools = [...CATALOG_TOOLS, ...(owner.customToolboxItems || [])];
const toolboxTools = allTools.filter(tool => !outsideToolIds.has(tool.id));
const COLOR_ICONS = {
'workflow-transfer': '🔄',
'prompt-studio': '🎛️',
'prompt-translator': '🌐',
'model-sources': '🔗',
'prompt-notes': '📝',
};
toolboxTools.forEach(tool => {
const tile = document.createElement('div');
tile.className = 'anomalous-toolbox-tile';
tile.setAttribute('data-tool-id', tool.id);
const iconEl = document.createElement('div');
iconEl.className = 'anomalous-toolbox-tile-icon';
iconEl.innerHTML = COLOR_ICONS[tool.id] || tool.icon || '🔧';
tile.appendChild(iconEl);
const labelEl = document.createElement('div');
labelEl.className = 'anomalous-toolbox-tile-label';
labelEl.textContent = t(tool.labelKey) || t(tool.nameKey);
tile.appendChild(labelEl);
tile.onclick = (e) => {
e.stopPropagation();
toolboxModal.style.display = 'none';
toolboxBtn.classList.remove('is-active');
executeToolAction(tool.id);
};
gridContainer.appendChild(tile);
});
toolboxModal.appendChild(gridContainer);
};
owner.renderToolboxModal = renderToolboxModal;
owner.registerToolboxItem = (item) => {
owner.customToolboxItems = owner.customToolboxItems || [];
owner.customToolboxItems.push(item);
if (owner.renderToolboxModal) owner.renderToolboxModal();
};
owner.openModelSources = (scope = 'workflow') => openModelSourcesModal(scope);
container.appendChild(toolboxModal);
const close = () => {
toolboxModal.style.display = 'none';
toolboxBtn.classList.remove('is-active');
document.removeEventListener('mousedown', closeToolbox);
};
return {
button: toolboxBtn,
modal: toolboxModal,
close,
mount() {
renderToolboxModal();
renderShortcutActions();
},
refreshLanguage() {
renderToolboxModal();
renderShortcutActions();
},
};
}