diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f54c95a..d895a6f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -296,7 +296,7 @@ covered by the `styles.css` manifest. difference); `activity_log.js` is the client and the words for each entry; `ui_activity.js` renders the activity page and the home page's recent list. `ui_settings_hub.js` owns settings and model-card preferences; - `ui_toolbox.js` owns the tool catalog, the rail's tool buttons, and tool dispatch; + `ui_rail_tools.js` owns the rail's tool buttons (scan, doctor, current node); `ui_browser_navigation.js` owns shared panel hiding/cleanup and workspace return, including Esc on the workspace panel (`nbPanel`, below the header and right of the rail; the list column steps aside while it is open). @@ -315,10 +315,10 @@ covered by the `styles.css` manifest. shortcuts, their fallback when ComfyUI's keybinding does not fire, and the settings control that opens ComfyUI's keybinding editor. - `ui_model_sources.js` owns the Model Sources Hub, managing workflow-model and global-library source detection, Civitai/HuggingFace URL attribution, sidecar persistence, and resilient scope switching between active workflow and full local library (with cached library state preservation and reliable re-rendering). -- `ui_materials.js`, `ui_material_cards.js`, and `ui_material_application.js` own the Material Library UI, category navigation, card presentation (with grab cursor affordances, explicit drag tooltips, and polymorphic card dragging via `bindPolymorphicMaterialCardDrag`), context-aware drag guidance, relaxed third-party node prompt widget sniffing and injection, and the structured empty state onboarding blueprint guiding users through collection, canvas drag, and prompt studio mixing. Drag precedence prioritizes node hits over blank canvas drops; blank canvas drops auto-instantiate `CLIPTextEncode` nodes for prompt materials with standard colors or open full workflows. `node_material_actions.js` owns prompt envelope extraction (`extractMaterialPromptEnvelope`) shared by detail views, cards and canvas actions. +- `ui_materials.js`, `ui_material_cards.js`, and `ui_material_application.js` own the Material Library UI, category navigation, card presentation (with grab cursor affordances, explicit drag tooltips, and polymorphic card dragging via `bindPolymorphicMaterialCardDrag`), context-aware drag guidance, prompt drops into the prompt box under the pointer (`prompt_drop.js`), and the structured empty state onboarding blueprint guiding users through collection, canvas drag, and prompt studio mixing. Drag precedence prioritizes node hits over blank canvas drops; blank canvas drops create `CLIPTextEncode` nodes (one per side the prompt has) or open full workflows. `node_material_actions.js` owns prompt envelope extraction (`extractMaterialPromptEnvelope`) shared by detail views, cards and canvas actions. - `ui_update_guide.js` and `update_guide_data.js` own the non-intrusive update guide modal (accessible via header button `#anomalous-update-notice-btn`, the Help modal and Home's "What's new"; the current guide's ID and steps live in `update_guide_data.js`) with full bilingual localization. `ui_spotlight_tour.js` provides the interactive spotlight mask tour (`startSpotlightTour`), gliding smooth focal box highlights across the rail's pages and tools with directional tooltip cards and keyboard navigation; steps whose target is not on screen are skipped. Other views pass their own `steps` (text from locale keys) and an optional `onClose`; the GPT-SoVITS import window does. -- `tool_registry.js` holds the tools' names, icons and stable IDs: the catalog tools (including Prompt Notes / 提示词笔记) and the two anchors (Toolbox and Settings). -- The rail's tool slots (built in `ui_toolbox.js`, fixed) hold scan, doctor and assistant, then the toolbox; settings sits at the rail's bottom. Materials is a rail page. The Toolbox popover lists the remaining catalog tools and opens beside the rail. +- `tool_registry.js` holds the tool icons shared by the rail and Home. +- The rail's tool slots (`ui_rail_tools.js`, fixed) hold scan, doctor and current node; settings sits at the rail's bottom. Materials is a rail page. There is no toolbox: the other tools open from their pages (workflow share codes on Workflows, Prompt Studio and Prompt Notes on Materials, Model Sources on Models and in the doctor, translation in Current node). - `ui_model_types.js` owns the models page's type chips (one per models folder, with its count) and `owner.modelScope`, what the grid lists: a whole type, or one list folder shown as a crumb. The grid's cards set `currentType/PathIdx/Subfolder` to their own model's folder (`focusModel`), @@ -348,7 +348,7 @@ covered by the `styles.css` manifest. unfolded companion models card with unconstrained multi-column tile flow eliminating nested gallery scrollbars, prompt composer with dynamic field-sizing and compact inline find & replace toolbar, flat material library archiving card with clean single-icon feedback, and unified dark slim scrollbar ergonomics with complete bilingual dictionary coverage in `locales.js`), and - `notebook_canvas.js` owns LiteGraph creation. Prompt Notes are integrated as a standard tool in the Toolbox + `notebook_canvas.js` owns LiteGraph creation. Prompt Notes open from the Material Library's top bar with defensive workspace return state restoration, TDZ-safe summary initialization, and responsive empty-state fallback rendering. `ui_recipe_detail.js` coordinates the Workflow Recipe detail session and model composition. `ui_recipe_overview.js` owns the Overview prompt showcase (with `entry.text` fallback, guarded non-shrinking primary action CTA, and floating Popover More dropdown menu), and `ui_recipe_parameters.js` diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e4379..17f359c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ - **Prompt boxes found by what they are (提示词框按节点定义识别)**: dropping or writing a prompt (material library, Prompt Studio, translator) now finds a node's prompt boxes by ComfyUI's own definition (multiline text inputs) instead of guessing from names, so a filename or "value" box no longer takes a prompt and third-party prompt nodes are recognised. Whether a box is positive or negative follows its wiring to the sampler, not the node's title: a CLIP Text Encode connected to the negative input gets the negative prompt even if it is still called "CLIP Text Encode". Prompt Studio's "take prompts from the selected node" also reads the roles right again (it could not follow links in current ComfyUI). - **See where a dragged prompt goes (拖提示词前先看清楚落在哪)**: while you drag a prompt from the material library, every prompt box on the canvas is outlined (green for positive, red for negative, grey when it is not wired to a sampler) and the box under the pointer lights up. The label next to the pointer says exactly what releasing does, e.g. "Release: into #6's positive prompt box; the negative prompt box on the same sampler (#7) too". Drop on a box and that box is filled; the matching negative (or positive) box of the same sampler gets the other half of the material. On a node with several prompt boxes, point at the one you mean instead of letting it guess. A short message confirms what was filled; the library's receipt undoes it. Dropping a prompt plan on empty canvas used to create an empty prompt node; it now creates a positive and, when the plan has one, a negative node with their text. - **Current node (当前节点, formerly Node Assistant)**: the panel no longer splits into "actions" and "parameter sets" tabs; it is one page with the model actions on top and **Parameters** below. Parameters lists every saved set of values for this kind of node in one list — from the material library and from recipes' parameter sets, the same values saved twice shown once — and each entry says exactly what it would change on the selected node ("steps: 20 → 30"). **Apply these N** changes only those; seeds and model files stay (change the model above), and the receipt undoes it. **Save these values** keeps the node's current values in the material library, so the next node of that kind can use them. -- **Translation moved onto the node (翻译挪到节点上)**: Current node now lists the selected node's prompt boxes, each marked positive or negative by its wiring. A box written in Chinese has **Translate to English**, which writes the English back (the receipt undoes it); an English box has **Show in Chinese**, which shows the meaning underneath and writes nothing. The separate Prompt Translator window is gone; Prompt Studio keeps its own translate buttons. +- **Translation moved onto the node (翻译挪到节点上)**: Current node now lists the selected node's prompt boxes, each marked positive or negative by its wiring. A box written in Chinese has **Translate to English**, which writes the English back (the receipt undoes it); an English box has **Show in Chinese**, which shows the meaning underneath and writes nothing. The separate Prompt Translator window is gone; Prompt Studio keeps its own translate buttons. +- **No more toolbox (工具箱拆掉了)**: every tool now opens from the page it belongs to. Workflow share codes (import / export) are the ⇅ button at the top right of **Workflows** (where a disabled import button used to sit); **Prompt Studio** and **Prompt Notes** are at the top right of **Materials**; **Model Sources** is at the end of the type chips on **Models**, and Model Doctor has **Where to download** for the open workflow's models; translation is on the prompt boxes in **Current node**. The rail keeps scan, Model Doctor and Current node. ### 🪶 Lighter on memory (更省内存) - The output gallery shows 512 px thumbnails instead of full images (cached like model cards), so scrolling through hundreds of outputs no longer fills the browser's memory and graphics memory with full-size pictures. Dragging an image to the canvas and the large viewer still use the original. When the browser has been closed for 90 seconds, its gallery images are released too, like the model grid. diff --git a/README.md b/README.md index c224645..6dd9e45 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Tools on the rail: * **Scan** reads your model folders and fetches covers, trigger words and base models from Civitai (by file hash, no extra Python packages). * **Model Doctor** checks each workflow you open: when models are missing, a bar over the canvas says so and puts back, with one press, the ones you have under another name or folder (recognised by hash and file size); its page lists the rest with what you can do. * **Current node** (formerly Node Assistant) shows the model of the node you select, lets you swap it from the covers or insert a LoRA, and lists every set of values saved for that kind of node with exactly what applying it would change. -* **Toolbox** holds the rest: workflow share codes (import / export), Prompt Studio, the prompt translator, Model Sources (where each model can be downloaded) and Prompt Notes. +* **Each tool on its page**: workflow share codes (import / export) on Workflows, Prompt Studio and Prompt Notes on Materials, Model Sources (where each model can be downloaded) on Models and in Model Doctor, translation on the prompt boxes in Current node. * **Settings** (at the bottom): language, font size, thumbnails, video covers, which model folders to show, and feedback. **Light on your computer**: the plugin itself loads no models, so it takes no graphics memory from image generation; the browser shows small thumbnails instead of full images, and its pictures are released a minute and a half after you close it. The voice models of Anomalous_TTS give their graphics memory back whenever ComfyUI needs it for image generation, and all of it on **Unload models**. @@ -107,7 +107,7 @@ The first time you open it, start on **Home**: **Scan model folders** fills in c * **扫描**:读取模型文件夹,按文件哈希从 C 站获取封面、触发词和底模。不需要额外安装 Python 包。 * **模型医生**:打开工作流时自动检查,缺模型就在画布上方提示;本地改过名、换过文件夹的同一个文件(按哈希和文件大小认)一键换上,其余的在医生页里自己挑或去 Civitai 找。 * **当前节点**(原“节点助手”):选中画布上的节点,查看它用的模型。可以看图换模型、插入 LoRA;存过的同类节点参数列在一起,每条写明会改哪几项,一键套用。 -* **工具箱**:其余工具都在这里,包括工作流分享码(导入导出)、提示词工坊、翻译助手、模型来源(每个模型去哪下载)和提示词笔记。 +* **工具回到各自的页面**:工作流分享码(导入导出)在“工作流”页,提示词工坊和提示词笔记在“素材”页,模型来源(每个模型去哪下载)在“模型”页和模型医生里,翻译在“当前节点”的提示词框上。 * **设置**(最下面):语言、字号、缩略图、视频封面、显示哪些模型文件夹,以及提交反馈。 **不占你的资源**: diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index fff8686..47844b6 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -6,7 +6,7 @@ localization, media, or explicit canvas mutations. ## Bootstrap and module ownership The verified AMB0/AMB1 workflow share-code Import / Export Center is available -from Toolbox through `window.AMB_WorkflowShare.showUnifiedModal()`. Both directions +from the Workflows page's top bar (⇅) through `window.AMB_WorkflowShare.showUnifiedModal()`. Both directions are enabled by explicit product decision. It is independent of the paused Recipe package import/export and closed Material Library file import. Image/workflow hash injection, host saving, and ordinary image downloads remain unchanged. @@ -44,7 +44,7 @@ icon rail (`ui_shell_rail.js`), page navigation and the list column's open state (`ui_shell_nav.js`), the home page (`ui_home.js`) and the window frame (`ui_shell_frame.js`) are their own modules. Every page change goes through `owner.goTo(page)`. `ui_settings_hub.js` owns settings and model-card preferences, -while `ui_toolbox.js` owns the catalog, the rail's tool buttons, and tool dispatch. +while `ui_rail_tools.js` owns the rail's tool buttons. The Material Library shortcut remains a native ComfyUI command/keybinding; a deferred window-key fallback invokes the same command path only when the host did not bring the library forward, so handled shortcuts are not executed twice. diff --git a/docs/architecture/material-library.md b/docs/architecture/material-library.md index 18c07a2..4429d4f 100644 --- a/docs/architecture/material-library.md +++ b/docs/architecture/material-library.md @@ -239,7 +239,7 @@ Both containers share the browser's panel area but have separate headers and lifecycle state; switching restores the appropriate container. Material Library file import and its transfer-center entry are closed. It has no independent image-material bundle exporter. The verified workflow share-code Import / Export -Center lives in Toolbox. Recipe package import/export remains paused, while prompt-plan +Center opens from the Workflows page's top bar. Recipe package import/export remains paused, while prompt-plan JSON export has been removed. Local capture/save actions remain available. Opening the library with exactly one live selected node enables apply mode. diff --git a/docs/architecture/recipes.md b/docs/architecture/recipes.md index 76c29c0..4cb2892 100644 --- a/docs/architecture/recipes.md +++ b/docs/architecture/recipes.md @@ -21,7 +21,7 @@ local saves are unaffected. Reopening requires an explicit release decision, package round-trip and failure validation, then restoring the frontend export action and enabling the backend gates together. Do not reopen package transfers as a side effect of UX work. Workflow -share-code import/export is a separate, verified feature available from Toolbox. +share-code import/export is a separate, verified feature on the Workflows page's top bar (⇅). ## Product and data model diff --git a/web/modules/browser.js b/web/modules/browser.js index 3b3a528..61a64a3 100644 --- a/web/modules/browser.js +++ b/web/modules/browser.js @@ -19,7 +19,7 @@ import { openPromptStudio } from './ui_prompt_composer.js'; import { closeUpdateGuide } from './ui_update_guide.js'; import { showImageWorkbench } from './ui_gallery_detail.js'; import { openDoctorPage } from './ui_doctor.js'; -import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, diagnoseNode, openLoraInsertionPicker } from './ui_node_assistant.js'; +import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, diagnoseNode, openCurrentNode, openLoraInsertionPicker } from './ui_node_assistant.js'; import { _openGalleryReplacer } from './ui_node_model_picker.js'; import { renderAudioStudio, stopAudioStudioPlayback } from './ui_audio_studio.js'; import { renderAudioGallery, stopGalleryAudio } from './ui_audio_gallery.js'; @@ -138,6 +138,7 @@ export class AnomalousBrowser { AnomalousBrowser.prototype.openDoctorPage = function () { openDoctorPage(this); }; AnomalousBrowser.prototype.diagnoseNode = diagnoseNode; +AnomalousBrowser.prototype.openCurrentNode = function () { openCurrentNode(this); }; AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel; AnomalousBrowser.prototype.renderAssistantModelCard = renderAssistantModelCard; AnomalousBrowser.prototype._loadAssistantHistory = _loadAssistantHistory; diff --git a/web/modules/locales.js b/web/modules/locales.js index b5f0212..faf57a6 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -1,11 +1,5 @@ export const i18n = { zh: { - actionTools: "工具", - actionScan: "扫描", - actionDoctor: "医生", - actionAssistant: "节点", - actionMaterials: "素材", - actionSettings: "设置", updateGuideTitle: "本次更新", updateGuideReplay: "查看更新引导", updateGuideSkip: "跳过", @@ -418,7 +412,7 @@ export const i18n = {

📖 使用说明

🧪 测试功能与数据保护
工作流配方、素材库和“当前节点”的参数仍在测试阶段。更新插件、导入别人的配方、恢复版本、删除配方或应用参数前,请先保存当前画布,并备份 ComfyUI 用户目录里的 workflows/anomalous_recipes、workflows/anomalous_materials 与 workflows/anomalous_parameters。
-

1. 🧭 左侧图标栏
每个页面一个图标:主页、记录、模型、图库、工作流、素材,以及角色语音和音频库。再点一下当前页的图标,或顶栏最左边的按钮,可以收起、展开旁边的列表;窗口较窄时,列表会变成从左边滑出的抽屉。图标栏下半部分是工具:扫描、模型医生、当前节点和工具箱,最下面是设置。

+

1. 🧭 左侧图标栏
每个页面一个图标:主页、记录、模型、图库、工作流、素材,以及角色语音和音频库。再点一下当前页的图标,或顶栏最左边的按钮,可以收起、展开旁边的列表;窗口较窄时,列表会变成从左边滑出的抽屉。图标栏下半部分是工具:扫描、模型医生和当前节点,最下面是设置。

2. 🔄 先扫描一次
点图标栏的 扫描(或主页的“扫描模型文件夹”)。插件按文件哈希从 C 站获取封面、触发词和底模,需要联网;没匹配上的模型可以在扫描页“重新联网查找”,部分限制级模型需要在扫描页的“高级选项”里填写 C 站 API Key。扫描在后台进行,可以关掉窗口。扫描完成后,其余功能才有完整的模型信息。

3. 📦 模型
顶部标签按类型(Checkpoint、LoRA、VAE…)切换,连同子文件夹一起列出;在左侧列表点某个文件夹只看那一个,点标签末尾的 ✕ 回到整个类型。卡片上的 + 在画布上创建加载节点,铅笔编辑名称、备注和封面,雷达图标只扫描这一个模型。

4. 🩺 模型医生
打开别人的工作流或图片、模型报红时点它:按哈希和文件大小在本地找到对应的模型,一键替换。“查看哈希”可以逐项比对。

@@ -427,7 +421,7 @@ export const i18n = {

7. ✨ 素材
在图库或配方里打开一张图的参数详情,可以把原图、完整工作流和节点参数存成素材。拖到画布空白处打开工作流(提示词素材会生成提示词节点),拖到已有节点上则把参数填进去。

8. 🎙️ 角色语音
需要另外安装 Anomalous_TTS 节点包。导入 GPT-SoVITS 角色、设置情绪和读音、在剧本台生成语音;音频页右上角的 ! 有分步说明和遮罩导览。

9. 🕘 操作记录
按天列出插件改过的东西:画布上节点设置的变化、加删的节点、打开的工作流,以及模型、封面、配方、素材等文件。只记录、不撤销;你自己在画布上的修改不会记进来。

-

10. 🧰 工具箱
导入导出(AMB0/AMB1 工作流分享码)、提示词工坊、翻译助手、模型来源和提示词笔记。

+

10. 🧰 其他工具在哪
工作流分享码(AMB0/AMB1 导入导出)在“工作流”页右上角 ⇅;提示词工坊和提示词笔记在“素材”页右上角;模型来源在“模型”页类型标签的最右边,工作流里模型的来源在模型医生里;翻译在“当前节点”的提示词框上。

11. ⚙️ 设置
语言、字号、缩略图和视频封面。文件夹管理 可以隐藏不需要的模型文件夹、调整顺序,隐藏的文件夹不再读取和扫描。💬 提交反馈 / 报告问题 直达 GitHub Issues。

`, notebooks: '笔记本', @@ -740,6 +734,7 @@ export const i18n = { doctorUseCandidate: '用它', doctorPick: '自己挑', doctorRecheck: '重新检查', + doctorSources: '模型去哪下载', doctorNoModels: '当前工作流没有用到模型文件。', doctorAllHere: '{count} 个模型都在本机,可以直接运行。', doctorHowItWorks: '“换上”只会用同一个文件(指纹一致)。其余的请自己挑一个本地模型,或者去 Civitai 下载。', @@ -970,7 +965,7 @@ export const i18n = { sidebarAssistant: '当前节点', sidebarPreflight: '预检导入工作流', sidebarSettings: '设置中心', - sidebarScanWizard: '📡 模型扫描向导', + sidebarScan: '扫描模型', sidebarScanComplete: '✅ 扫描/重命名已完成!数据已为您更新。', sidebarGlobalWizardTitle: '全局扫描向导', sidebarProvenance: '在工作流里记下模型指纹', @@ -1016,33 +1011,15 @@ export const i18n = { sidebarBgAtmosphereHelp: '调节底图星图蓝图的可见度(0%为纯净黑曜石)', sidebarResetLayout: '↺ 恢复默认窗口布局', sidebarResetConfirm: '确认重置窗口位置、缩放和停靠状态吗?', - sidebarToolbox: '实用工具箱', - toolboxTitle: '实用工具箱', promptLibraryAutoSync: "素材库词卡自动同步", promptLibrarySyncFailed: "素材库同步失败,已保留现有词卡,将自动重试", promptLibraryBadge: "🔒 来自素材库", promptLibrarySource: "来自素材库:{name}({filename})", promptLibraryReadOnly: "由源素材管理,不能在工坊删除;拼装台中的副本可自由编辑。", - toolWorkflowTransferShort: "导入导出", toolWorkflowTransferTitle: "工作流导入导出中心", - toolboxSubtitle: '模型维护与拓展辅助工具', - toolboxPlanned: '规划中', - toolboxReady: '就绪', - toolboxHintFooter: '💡 持续扩展中:模型批量重整、孤立缓存清理、哈希重算等运维工具将在此聚合。', - toolPromptStudioTitle: '提示词工坊', toolPromptStudioShort: '提示词工坊', - toolOrphanCleanerTitle: '孤立缓存与临时文件清理', - toolOrphanCleanerShort: '缓存清理', - toolOrphanCleanerDesc: '扫描并清理已删除模型的未引用缩略图缓存与临时残余文件。', - toolBatchRenameTitle: '批量命名与标签重整', - toolBatchRenameShort: '批量重整', - toolBatchRenameDesc: '对指定目录下的模型批量规范命名规则或批量附加分类标签。', - toolCivitaiJumpTitle: 'Civitai 页面一键直达', - toolCivitaiJumpShort: 'Civitai', - toolCivitaiJumpDesc: '根据模型 SHA256 哈希或名称直接在浏览器打开对应发布页。', toolModelSourcesTitle: '模型来源与发布页中控', toolModelSourcesShort: '模型来源', - toolPromptNotesTitle: '提示词笔记', toolPromptNotesShort: '提示词笔记', modelSourcesModalTitle: '🌐 模型来源统一中控中心', modelSourcesScopeWorkflow: '当前工作流', @@ -1084,15 +1061,6 @@ export const i18n = { modelSourcesLocalTargetUnknown: '尚未确定本地文件位置,可先将链接保存到工作流。', modelSourcesNoModelsFound: '未检测到任何模型', modelSourcesUrlPlaceholder: '输入或粘贴来源链接 (Civitai / Hugging Face / Liblib / 网盘 等)...', - toolHashRecalcTitle: '哈希快查', - toolHashRecalcShort: '哈希快查', - toolHashRecalcDesc: '快速计算并校验模型文件的 SHA256 哈希值。', - toolFormatConvertTitle: '格式转换', - toolFormatConvertShort: '格式转换', - toolFormatConvertDesc: '模型权重的 FP8/FP16 精度或格式互转辅助工具。', - toolCustomScriptsTitle: '扩展脚本', - toolCustomScriptsShort: '扩展脚本', - toolCustomScriptsDesc: '通过开放接口注册并执行自定义 Python / JS 运维动作。', sidebarInjectHash: '🟢 注入工作流哈希', sidebarSkipHash: '⚪ 不注入工作流哈希', sidebarManageFolders: '文件夹管理', @@ -1169,7 +1137,6 @@ export const i18n = { recipeNoMatches: '没有匹配的配方。请清除搜索或标签筛选。', recipeFilterByTag: '按此标签筛选', recipeImport: '导入配方包', - recipeImportUnavailable: '配方包导入暂未开放', recipeExport: '导出配方包', recipeExportUnavailable: '配方包导出暂未开放', recipeModel: '模型', @@ -1904,12 +1871,6 @@ export const i18n = { scriptDirectorSampleNotListed: "{node} 节点的列表里还没有 {file}。请先在 ComfyUI 中刷新节点定义(按 R)后再试。" }, en: { - actionTools: "Tools", - actionScan: "Scan", - actionDoctor: "Fix", - actionAssistant: "Node", - actionMaterials: "Lib", - actionSettings: "Prefs", updateGuideTitle: "What's new", updateGuideReplay: "View update guide", updateGuideSkip: "Skip", @@ -2322,7 +2283,7 @@ export const i18n = {

📖 How to use

🧪 Preview features and your data
Workflow recipes, the Material Library and Current node's parameters are still in preview. Before updating the plugin, importing someone else's recipe, restoring a version, deleting a recipe or applying parameters, save your canvas and back up workflows/anomalous_recipes, workflows/anomalous_materials and workflows/anomalous_parameters in your ComfyUI user folder.
-

1. 🧭 The rail on the left
One icon per page: Home, Activity, Models, Gallery, Workflows, Materials, then Voices and Audio. Click the current page's icon again, or the button at the far left of the header, to hide or show its list; in a narrow window the list slides in as a drawer. Below the pages are the tools: Scan, Model Doctor, Current node and the Toolbox, with Settings at the bottom.

+

1. 🧭 The rail on the left
One icon per page: Home, Activity, Models, Gallery, Workflows, Materials, then Voices and Audio. Click the current page's icon again, or the button at the far left of the header, to hide or show its list; in a narrow window the list slides in as a drawer. Below the pages are the tools: Scan, Model Doctor and Current node, with Settings at the bottom.

2. 🔄 Scan once first
Click Scan on the rail (or "Scan model folders" on Home). The plugin fetches covers, trigger words and base models from Civitai by file hash, so it needs the internet; models that were not matched can be looked up again from the scan page, and some restricted models need a Civitai API key, entered under Advanced on the scan page. The scan runs in the background, so you can close the window. Once scanned, every other feature has full model information.

3. 📦 Models
The chips at the top switch between types (Checkpoint, LoRA, VAE…) and list the whole type, subfolders included; click a folder in the list to see only that one, and the ✕ at the end of the chips to go back. On a card, + adds a loader node to the canvas, the pencil edits name, notes and cover, and the radar icon scans just that model.

4. 🩺 Model Doctor
When someone else's workflow or image opens with red, missing models, click it: it finds your local copy of each model by hash and file size and swaps it in. "View hash" compares them side by side.

@@ -2331,7 +2292,7 @@ export const i18n = {

7. ✨ Materials
Open an image's parameter details in the Gallery or a recipe to save the image, its full workflow and its node settings as a material. Drop it on an empty canvas to open the workflow (prompt materials create prompt nodes), or on a node to fill in its settings.

8. 🎙️ Character voices
Needs the Anomalous_TTS node pack. Import GPT-SoVITS characters, set emotions and pronunciations, and generate speech in the Script Director; the ! at the top right of the audio page has a step-by-step guide and a spotlight tour.

9. 🕘 Activity
What the plugin changed, by day: node settings on the canvas, nodes added or removed, workflows opened, and files such as models, covers, recipes and materials. It records only and undoes nothing; your own edits on the canvas are not listed.

-

10. 🧰 Toolbox
Import / export (AMB0/AMB1 workflow share codes), Prompt Studio, the prompt translator, Model Sources and Prompt Notes.

+

10. 🧰 Where the other tools are
Workflow share codes (AMB0/AMB1 import / export): ⇅ at the top right of Workflows. Prompt Studio and Prompt Notes: top right of Materials. Model Sources: at the end of the type chips on Models; the sources of the open workflow's models: in Model Doctor. Translation: on the prompt boxes in Current node.

11. ⚙️ Settings
Language, font size, thumbnails and video covers. Folder manager hides model folders you do not need and orders them; hidden folders are not read or scanned. 💬 Feedback / report an issue opens GitHub Issues.

`, notebooks: 'Notebooks', @@ -2644,6 +2605,7 @@ export const i18n = { doctorUseCandidate: 'Use it', doctorPick: 'Pick one', doctorRecheck: 'Check again', + doctorSources: 'Where to download', doctorNoModels: 'The open workflow uses no model files.', doctorAllHere: 'All {count} models are on this computer; the workflow can run.', doctorHowItWorks: '"Put in" only uses the same file (same fingerprint). For the others, pick a local model yourself or download it from Civitai.', @@ -2874,7 +2836,7 @@ export const i18n = { sidebarAssistant: 'Current node', sidebarPreflight: 'Preflight Import', sidebarSettings: 'Settings Hub', - sidebarScanWizard: '📡 Model Scan Wizard', + sidebarScan: 'Scan models', sidebarScanComplete: '✅ Scan/Rename completed! Data updated.', sidebarGlobalWizardTitle: 'Global Scan Wizard', sidebarProvenance: 'Record model fingerprints in workflows', @@ -2920,33 +2882,15 @@ export const i18n = { sidebarBgAtmosphereHelp: 'Adjust blueprint backdrop opacity (0% is pure solid dark)', sidebarResetLayout: '↺ Reset Window Layout', sidebarResetConfirm: 'Reset window position, scale and dock state?', - sidebarToolbox: 'Toolbox Hub', - toolboxTitle: 'Toolbox Hub', promptLibraryAutoSync: "Material Library cards sync automatically", promptLibrarySyncFailed: "Library sync failed. Existing cards are kept; retrying automatically.", promptLibraryBadge: "🔒 From Material Library", promptLibrarySource: "Material Library: {name} ({filename})", promptLibraryReadOnly: "Managed by the source material; cannot be deleted here. Mixer copies remain editable.", - toolWorkflowTransferShort: "Import / Export", toolWorkflowTransferTitle: "Workflow Import / Export Center", - toolboxSubtitle: 'Model Maintenance & Utility Tools', - toolboxPlanned: 'Planned', - toolboxReady: 'Ready', - toolboxHintFooter: '💡 Expanding: Batch organizer, orphan cache cleaner, and hash recalculation tools will be hosted here.', - toolPromptStudioTitle: 'Prompt Studio', toolPromptStudioShort: 'Prompt Studio', - toolOrphanCleanerTitle: 'Orphan Cache & Temp Cleaner', - toolOrphanCleanerShort: 'Cache Clean', - toolOrphanCleanerDesc: 'Scan and remove unreferenced thumbnails and temporary files for deleted models.', - toolBatchRenameTitle: 'Batch Rename & Tagging', - toolBatchRenameShort: 'Batch Rename', - toolBatchRenameDesc: 'Standardize model filenames or batch-assign taxonomy tags to folders.', - toolCivitaiJumpTitle: 'Civitai Quick Jump', - toolCivitaiJumpShort: 'Civitai', - toolCivitaiJumpDesc: 'Quickly open official model release pages in browser by SHA256 hash or title.', toolModelSourcesTitle: 'Model Source Hub', toolModelSourcesShort: 'Sources', - toolPromptNotesTitle: 'Prompt Notes', toolPromptNotesShort: 'Prompt Notes', modelSourcesModalTitle: '🌐 Model Source Hub', modelSourcesScopeWorkflow: 'Active Workflow', @@ -2988,15 +2932,6 @@ export const i18n = { modelSourcesLocalTargetUnknown: 'Local file location is not resolved. You can save the link to the workflow.', modelSourcesNoModelsFound: 'No models detected', modelSourcesUrlPlaceholder: 'Enter or paste source URL (Civitai / Hugging Face / Liblib / etc)...', - toolHashRecalcTitle: 'Hash Check', - toolHashRecalcShort: 'Hash Check', - toolHashRecalcDesc: 'Quickly compute and verify model file SHA256 hash.', - toolFormatConvertTitle: 'Format Convert', - toolFormatConvertShort: 'Convert', - toolFormatConvertDesc: 'Helper tools for model FP8/FP16 precision and format conversion.', - toolCustomScriptsTitle: 'Custom Scripts', - toolCustomScriptsShort: 'Scripts', - toolCustomScriptsDesc: 'Register and run custom Python / JS maintenance scripts via API.', sidebarInjectHash: '🟢 Inject Workflow Hash', sidebarSkipHash: '⚪ Skip Workflow Hash', sidebarManageFolders: 'Manage Folders', @@ -3072,7 +3007,6 @@ export const i18n = { recipeNoMatches: 'No recipes match. Clear the search or tag filters.', recipeFilterByTag: 'Filter by this tag', recipeImport: 'Import package', - recipeImportUnavailable: 'Recipe package import is temporarily unavailable', recipeExport: 'Export package', recipeExportUnavailable: 'Recipe package export is temporarily unavailable', recipeDialogNo: 'No', diff --git a/web/modules/tool_registry.js b/web/modules/tool_registry.js index 70bf6e3..a4072fa 100644 --- a/web/modules/tool_registry.js +++ b/web/modules/tool_registry.js @@ -1,117 +1,7 @@ -/** - * tool_registry.js - Centralized Tool Catalog for Anomalous Model Browser - * - * Provides stable IDs, display keys, icons, and metadata for both - * the Toolbox catalog and the customizable Shortcut Bar. - */ +/** Icons of the tools shared by the rail (ui_rail_tools.js, ui_shell_rail.js) and Home. */ export const TOOL_ICONS = Object.freeze({ - TOOLBOX: ``, - SCAN: ``, DOCTOR: ``, ASSISTANT: ``, MATERIALS: ``, - SETTINGS: ``, - TRANSFER: ``, - STUDIO: ``, - SOURCES: ``, - NOTEBOOK: ``, }); - -/** - * Fixed non-movable anchors: - * - Toolbox (left end) - * - Settings (right end) - */ -export const FIXED_ANCHORS = Object.freeze({ - TOOLBOX: Object.freeze({ - id: 'toolbox', - domId: 'anomalous-toolbox-btn', - labelKey: 'actionTools', - nameKey: 'sidebarToolbox', - icon: TOOL_ICONS.TOOLBOX, - isAnchor: true, - }), - SETTINGS: Object.freeze({ - id: 'settings', - domId: 'anomalous-global-settings-btn', - labelKey: 'actionSettings', - nameKey: 'sidebarSettings', - icon: TOOL_ICONS.SETTINGS, - isAnchor: true, - }), -}); - -/** - * All customizable tools available in the Toolbox catalog - * that can be pinned to the Shortcut Bar (up to 4 items). - */ -export const CATALOG_TOOLS = Object.freeze([ - Object.freeze({ - id: 'scan', - domId: 'anomalous-scan-btn', - labelKey: 'actionScan', - nameKey: 'sidebarScanWizard', - icon: TOOL_ICONS.SCAN, - }), - Object.freeze({ - id: 'doctor', - domId: 'anomalous-doctor-btn', - labelKey: 'actionDoctor', - nameKey: 'sidebarDoctor', - icon: TOOL_ICONS.DOCTOR, - }), - Object.freeze({ - id: 'assistant', - domId: 'anomalous-assistant-btn', - labelKey: 'actionAssistant', - nameKey: 'sidebarAssistant', - icon: TOOL_ICONS.ASSISTANT, - }), - Object.freeze({ - id: 'materials', - domId: 'anomalous-materials-btn', - labelKey: 'actionMaterials', - nameKey: 'materialLibrary', - icon: TOOL_ICONS.MATERIALS, - }), - Object.freeze({ - id: 'workflow-transfer', - domId: 'anomalous-transfer-btn', - labelKey: 'toolWorkflowTransferShort', - nameKey: 'toolWorkflowTransferTitle', - icon: TOOL_ICONS.TRANSFER, - }), - Object.freeze({ - id: 'prompt-studio', - domId: 'anomalous-studio-btn', - labelKey: 'toolPromptStudioShort', - nameKey: 'toolPromptStudioTitle', - icon: TOOL_ICONS.STUDIO, - }), - Object.freeze({ - id: 'model-sources', - domId: 'anomalous-sources-btn', - labelKey: 'toolModelSourcesShort', - nameKey: 'toolModelSourcesTitle', - icon: TOOL_ICONS.SOURCES, - }), - Object.freeze({ - id: 'prompt-notes', - domId: 'anomalous-prompt-notes-btn', - labelKey: 'toolPromptNotesShort', - nameKey: 'toolPromptNotesTitle', - icon: TOOL_ICONS.NOTEBOOK, - }), -]); - -export function getToolDefinition(toolId) { - if (!toolId) return null; - if (toolId === FIXED_ANCHORS.TOOLBOX.id || toolId === FIXED_ANCHORS.TOOLBOX.domId) return FIXED_ANCHORS.TOOLBOX; - if (toolId === FIXED_ANCHORS.SETTINGS.id || toolId === FIXED_ANCHORS.SETTINGS.domId) return FIXED_ANCHORS.SETTINGS; - return CATALOG_TOOLS.find(tool => tool.id === toolId || tool.domId === toolId) || null; -} - -export function getAllToolDefinitions() { - return [FIXED_ANCHORS.TOOLBOX, ...CATALOG_TOOLS, FIXED_ANCHORS.SETTINGS]; -} diff --git a/web/modules/ui_browser_navigation.js b/web/modules/ui_browser_navigation.js index 1b76c47..867050c 100644 --- a/web/modules/ui_browser_navigation.js +++ b/web/modules/ui_browser_navigation.js @@ -113,8 +113,6 @@ export function hideAllPanels() { this.currentDetailObserver.disconnect(); this.currentDetailObserver = null; } - const tbModal = document.getElementById('anomalous-toolbox-modal'); - if (tbModal) tbModal.style.display = 'none'; const setModal = document.getElementById('anomalous-settings-hub-modal'); if (setModal) setModal.style.display = 'none'; } diff --git a/web/modules/ui_doctor.js b/web/modules/ui_doctor.js index 2d00b73..013630e 100644 --- a/web/modules/ui_doctor.js +++ b/web/modules/ui_doctor.js @@ -9,6 +9,7 @@ import { translate as t } from './locales.js'; import { applyModelFix, checkWorkflowModels, fixWorkflowModels, isProblem, markReplaced, replacedFrom } from './model_check.js'; import { updateDoctorBanner } from './ui_doctor_banner.js'; +import { openModelSourcesModal } from './ui_model_sources.js'; const MODEL_EXT = /\.(safetensors|ckpt|pt|bin|pth|sft)$/i; const ORDER = ['fixable', 'candidate', 'conflict', 'ambiguous', 'missing', 'changed']; @@ -190,7 +191,8 @@ export async function renderDoctorPage(owner, panel, { refresh = false } = {}) { renderDoctorPage(owner, panel); })); } - actions.append(button('anomalous-scan-secondary', t('doctorRecheck'), () => renderDoctorPage(owner, panel, { refresh: true }))); + actions.append(button('anomalous-scan-secondary', t('doctorRecheck'), () => renderDoctorPage(owner, panel, { refresh: true })), + button('anomalous-scan-secondary', t('doctorSources'), () => openModelSourcesModal('workflow'))); page.append(actions); const verdict = !entries.length ? t('doctorNoModels') : !entries.some(isProblem) ? t('doctorAllHere', { count: entries.length }) diff --git a/web/modules/ui_home.js b/web/modules/ui_home.js index 5f45798..8f4fc6f 100644 --- a/web/modules/ui_home.js +++ b/web/modules/ui_home.js @@ -14,7 +14,7 @@ import { renderRecentActivity } from './ui_activity.js'; // One card per task: where it goes and its text (`homeCardTitle` / `…Body`). const CARDS = Object.freeze([ { name: 'Models', icon: PAGE_ICONS.models, open: owner => owner.goTo('models') }, - { name: 'Doctor', icon: TOOL_ICONS.DOCTOR, open: owner => owner.executeToolAction?.('doctor') }, + { name: 'Doctor', icon: TOOL_ICONS.DOCTOR, open: owner => owner.openDoctorPage() }, { name: 'Voices', icon: PAGE_ICONS.voices, open: owner => owner.goTo('voices') }, { name: 'Gallery', icon: PAGE_ICONS.gallery, open: owner => owner.goTo('gallery') }, { name: 'Recipes', icon: PAGE_ICONS.recipes, open: owner => owner.goTo('recipes') }, diff --git a/web/modules/ui_materials.js b/web/modules/ui_materials.js index 81bf9fb..cc069ba 100644 --- a/web/modules/ui_materials.js +++ b/web/modules/ui_materials.js @@ -109,6 +109,14 @@ function buildMaterialTopbar(owner) { owner.materialList?.classList.add('is-list'); }; + // Prompts are assembled from these materials, and prompt notes are kept beside them. + const studioBtn = text(right, 'button', t('toolPromptStudioShort'), 'anomalous-material-topbar-btn'); + studioBtn.type = 'button'; + studioBtn.onclick = () => owner.openPromptStudio(); + const notesBtn = text(right, 'button', t('toolPromptNotesShort'), 'anomalous-material-topbar-btn'); + notesBtn.type = 'button'; + notesBtn.onclick = () => owner.showNotebooks(); + // 刷新按钮 const refreshBtn = text(right, 'button', '↻', 'anomalous-material-topbar-btn'); refreshBtn.type = 'button'; diff --git a/web/modules/ui_model_types.js b/web/modules/ui_model_types.js index 867bec8..e37c37e 100644 --- a/web/modules/ui_model_types.js +++ b/web/modules/ui_model_types.js @@ -10,6 +10,7 @@ */ import { translate as t } from './locales.js'; +import { openModelSourcesModal } from './ui_model_sources.js'; const SCOPE_KEY = 'anomalous_model_scope'; @@ -130,5 +131,13 @@ export function renderTypeBar(owner, listed) { crumb.onclick = () => openType(owner, group); bar.appendChild(crumb); } + // Where each model can be downloaded (the Model Source Hub, library scope). + const sources = document.createElement('button'); + sources.type = 'button'; + sources.className = 'anomalous-model-type-chip is-tool'; + sources.textContent = t('toolModelSourcesShort'); + sources.title = t('toolModelSourcesTitle'); + sources.onclick = () => openModelSourcesModal('library'); + bar.appendChild(sources); return bar; } diff --git a/web/modules/ui_node_assistant.js b/web/modules/ui_node_assistant.js index e430f0b..cefd191 100644 --- a/web/modules/ui_node_assistant.js +++ b/web/modules/ui_node_assistant.js @@ -334,6 +334,15 @@ export function openLoraInsertionPicker(anchorNode, direction) { }); } +/** The rail's Current node button: the panel for the selected node, or the hint to select one. */ +export function openCurrentNode(owner) { + owner.enterToolPage?.('assistant'); + owner.hideAllPanels(); + owner.assistantPanel.style.display = 'flex'; + if (!owner.assistantPanelInitialized) owner.initAssistantPanel(); + owner.diagnoseNode(Object.values(app.canvas?.selected_nodes || {})[0] || null); +} + export function diagnoseNode(node) { if (!this.assistantPanelInitialized) { this.initAssistantPanel(); diff --git a/web/modules/ui_rail_tools.js b/web/modules/ui_rail_tools.js new file mode 100644 index 0000000..04f0eef --- /dev/null +++ b/web/modules/ui_rail_tools.js @@ -0,0 +1,35 @@ +/** + * The rail's tool buttons under the pages: scan, Model Doctor and Current node; settings + * at the rail's foot. Each opens its tool page; labels follow the language (the settings + * hub renders them again when it changes). The other tools live on their pages. + */ + +import { translate as t } from './locales.js'; +import { TOOL_ICONS } from './tool_registry.js'; +import { setScanButtonState } from './scan_runner.js'; + +const TOOLS = Object.freeze([ + { id: 'anomalous-scan-btn', nameKey: 'sidebarScan', open: owner => owner.openScanPage() }, + { id: 'anomalous-doctor-btn', nameKey: 'sidebarDoctor', icon: TOOL_ICONS.DOCTOR, open: owner => owner.openDoctorPage() }, + { id: 'anomalous-assistant-btn', nameKey: 'sidebarAssistant', icon: TOOL_ICONS.ASSISTANT, open: owner => owner.openCurrentNode() }, +]); + +/** Fills `owner.railTools` and `owner.railFoot`; `isScanning()` sets the scan button's state. */ +export function renderRailTools(owner, { isScanning, settingsButton }) { + owner.railTools.replaceChildren(...TOOLS.map((tool) => { + const button = document.createElement('button'); + button.id = tool.id; + button.className = 'anomalous-tooltip-target'; + button.setAttribute('aria-label', t(tool.nameKey)); + button.setAttribute('data-tooltip', t(tool.nameKey)); + button.setAttribute('data-tooltip-pos', 'right'); + if (tool.icon) button.innerHTML = tool.icon; // static markup only + else setScanButtonState(button, isScanning()); + button.onclick = (event) => { + event.stopPropagation(); + tool.open(owner); + }; + return button; + })); + owner.railFoot.replaceChildren(settingsButton); +} diff --git a/web/modules/ui_recipe_catalog.js b/web/modules/ui_recipe_catalog.js index d64cf7e..a5e2681 100644 --- a/web/modules/ui_recipe_catalog.js +++ b/web/modules/ui_recipe_catalog.js @@ -106,11 +106,12 @@ function buildRecipeStudioTopbar(owner) { viewSwitch.append(gridButton, listButton); right.appendChild(viewSwitch); - const importButton = appendText(right, 'button', '⇧', 'anomalous-recipe-topbar-btn anomalous-tooltip-target'); - importButton.type = 'button'; - importButton.disabled = true; - importButton.setAttribute('data-tooltip', t('recipeImportUnavailable')); - importButton.setAttribute('aria-label', t('recipeImportUnavailable')); + // Workflow share codes (AMB0/AMB1): import and export the canvas workflow (main.js). + const shareButton = appendText(right, 'button', '⇅', 'anomalous-recipe-topbar-btn anomalous-tooltip-target'); + shareButton.type = 'button'; + shareButton.setAttribute('data-tooltip', t('toolWorkflowTransferTitle')); + shareButton.setAttribute('aria-label', t('toolWorkflowTransferTitle')); + shareButton.onclick = () => window.AMB_WorkflowShare?.showUnifiedModal(); const saveButton = appendText(right, 'button', '', 'anomalous-recipe-topbar-btn is-primary'); saveButton.dataset.recipeSaveCurrent = 'true'; diff --git a/web/modules/ui_scan_watch.js b/web/modules/ui_scan_watch.js index 2998cbf..4ff441d 100644 --- a/web/modules/ui_scan_watch.js +++ b/web/modules/ui_scan_watch.js @@ -9,7 +9,7 @@ import { fetchLastScan, setScanButtonState, showScanResult } from './scan_runner const POLL_MS = 3000; -/** Starts polling; returns `isScanning()` for the toolbox's scan button. */ +/** Starts polling; returns `isScanning()` for the rail's scan button. */ export function watchScans(owner) { let scanning = false; setInterval(async () => { diff --git a/web/modules/ui_settings_hub.js b/web/modules/ui_settings_hub.js index c38fa58..3d42150 100644 --- a/web/modules/ui_settings_hub.js +++ b/web/modules/ui_settings_hub.js @@ -12,11 +12,9 @@ export function createSettingsHub(owner, { savedScale, savedBgOpacity, updateLangClass, - toolboxBtn, dockBtn, updateNoticeBtn, icons, - onBeforeOpen, }) { let refreshModelSettingsText = () => {}; @@ -38,13 +36,8 @@ export function createSettingsHub(owner, { const refreshLanguageUi = () => { langBtn.textContent = t(window.anomalous_browser_lang === 'zh' ? 'sidebarSwitchToEnglish' : 'sidebarSwitchToChinese'); updateLangClass(); - toolboxBtn.removeAttribute('title'); - toolboxBtn.setAttribute('aria-label', t('sidebarToolbox')); - toolboxBtn.setAttribute('data-tooltip', t('sidebarToolbox')); - toolboxBtn.setAttribute('data-tooltip-pos', 'right'); helpBtn.innerHTML = `${icons.HELP}${t('help')}`; - if (owner.renderToolboxModal) owner.renderToolboxModal(); - if (owner.renderShortcutActions) owner.renderShortcutActions(); + owner.renderRailTools?.(); const sBtn = document.getElementById('anomalous-global-settings-btn'); if (sBtn) { sBtn.removeAttribute('title'); sBtn.setAttribute('data-tooltip', t('sidebarSettings')); sBtn.setAttribute('data-tooltip-pos', 'right'); } @@ -567,7 +560,6 @@ modelSettingsNote.textContent = t('sidebarModelSettingsNote'); settingsBtn.onclick = (e) => { e.stopPropagation(); - onBeforeOpen(); if (settingsHubModal.style.display === 'none') { settingsHubModal.style.display = 'flex'; settingsBtn.classList.add('is-active'); diff --git a/web/modules/ui_shell_rail.js b/web/modules/ui_shell_rail.js index 7a43906..39dc900 100644 --- a/web/modules/ui_shell_rail.js +++ b/web/modules/ui_shell_rail.js @@ -1,7 +1,7 @@ /** * The shell's left icon rail: one entry per page (image pages, then the audio pages), - * the canvas tools and settings. Entries open pages through `owner.goTo(page)`; the - * toolbox module fills `owner.railTools` (toolbox, scan, doctor, assistant) and + * the canvas tools and settings. Entries open pages through `owner.goTo(page)`; + * ui_rail_tools.js fills `owner.railTools` (scan, doctor, current node) and * `owner.railFoot` (settings). Old ids are kept (`#anomalous-models-btn` …) so tours * and other modules still find them. */ diff --git a/web/modules/ui_sidebar.js b/web/modules/ui_sidebar.js index 41f0bbc..27dc578 100644 --- a/web/modules/ui_sidebar.js +++ b/web/modules/ui_sidebar.js @@ -8,7 +8,7 @@ import { escapeHtml } from './safe_dom.js'; import { showUpdateGuide } from './ui_update_guide.js'; import { AUDIO_USAGE_GUIDE, CURRENT_UPDATE_GUIDE } from './update_guide_data.js'; import { createSettingsHub } from './ui_settings_hub.js'; -import { createToolbox } from './ui_toolbox.js'; +import { renderRailTools } from './ui_rail_tools.js'; import { getActiveDomain } from './ui_domain_switcher.js'; import { renderAudioSidebar } from './ui_audio_sidebar.js'; import { createGallerySearchBar } from './ui_gallery.js'; @@ -27,7 +27,6 @@ const SIDEBAR_ICONS = { DOCK: ``, LIST: ``, HELP: ``, - TOOLBOX: ``, SETTINGS: ``, FOLDER: ``, CHEVRON_UP: ``, @@ -100,7 +99,7 @@ export function createDOM() { const savedViewMode = localStorage.getItem('anomalous_view_mode') || 'compact'; container.classList.add(`view-mode-${savedViewMode}`); - // Rail: pages, tools, settings. The toolbox fills its tool slots below. + // Rail: pages, tools, settings. ui_rail_tools.js fills its tool slots below. const rail = createShellRail(this); const brand = rail.root.querySelector('.anomalous-rail-brand'); brand.addEventListener('click', themeEasterEgg(brand)); @@ -225,26 +224,17 @@ export function createDOM() { watchWorkflowLoads(this); const isScanning = watchScans(this); - let settingsHubControl = null; - const toolboxControl = createToolbox(this, { - container, - toolboxIcon: SIDEBAR_ICONS.TOOLBOX, - isScanning, - getSettingsButton: () => settingsHubControl.button, - onBeforeOpen: () => settingsHubControl?.close(), - }); - settingsHubControl = createSettingsHub(this, { + const settingsHubControl = createSettingsHub(this, { container, savedScale, savedBgOpacity, updateLangClass, - toolboxBtn: toolboxControl.button, dockBtn, updateNoticeBtn, icons: SIDEBAR_ICONS, - onBeforeOpen: () => toolboxControl.close(), }); - toolboxControl.mount(); + this.renderRailTools = () => renderRailTools(this, { isScanning, settingsButton: settingsHubControl.button }); + this.renderRailTools(); bindShellResize(container); document.body.appendChild(this.modal); diff --git a/web/modules/ui_spotlight_tour.js b/web/modules/ui_spotlight_tour.js index 763fab2..491a13f 100644 --- a/web/modules/ui_spotlight_tour.js +++ b/web/modules/ui_spotlight_tour.js @@ -78,16 +78,6 @@ const TOUR_STEPS = Object.freeze([ bodyEn: 'Manages saved images, prompts, and workflow snippets. Drag a card onto a canvas node to inject values, or drop on empty canvas to load the workflow.', position: 'right', }, - { - id: 'toolbox', - targetSelector: '#anomalous-toolbox-btn', - icon: '🧰', - titleZh: '🧰 实用工具箱', - titleEn: '🧰 Utility Toolbox', - bodyZh: '点击展开九宫格面板,收纳了模型来源中心(检测工作流模型对应的 Civitai/HuggingFace 链接)、提示词笔记、文件夹管理等工具。', - bodyEn: 'Opens the drawer hosting Model Sources Hub (detects Civitai/HuggingFace links for workflow models), Prompt Notes, and Folder Manager.', - position: 'right', - }, { id: 'settings', targetSelector: '#anomalous-global-settings-btn', diff --git a/web/modules/ui_toolbox.js b/web/modules/ui_toolbox.js deleted file mode 100644 index 0d38c2a..0000000 --- a/web/modules/ui_toolbox.js +++ /dev/null @@ -1,266 +0,0 @@ -/** - * 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 './scan_runner.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.openScanPage(); - break; - case 'doctor': - owner.openDoctorPage(); - 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 '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 = '×'; - 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': '🎛️', - '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(); - }, - }; -} diff --git a/web/styles/01-shell-tools.css b/web/styles/01-shell-tools.css index 12f2875..c5ba28e 100644 --- a/web/styles/01-shell-tools.css +++ b/web/styles/01-shell-tools.css @@ -79,134 +79,11 @@ flex-shrink: 0 !important; } -@keyframes anomalous-popover-in { - 0% { - opacity: 0; - transform: translateY(6px) scale(0.96); - } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -#anomalous-toolbox-modal { - position: absolute; - bottom: 12px; - left: 68px; - width: 216px; - min-height: auto; - box-sizing: border-box; - background: rgba(22, 22, 26, 0.98); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 9px; - padding: 7px 7px 6px; - box-shadow: 0 18px 44px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(255, 255, 255, 0.08); - z-index: 999999 !important; - display: none; - flex-direction: column; - gap: 3px; - overflow: visible !important; -} - -#anomalous-toolbox-modal[style*="display: flex"] { - animation: anomalous-popover-in 0.18s cubic-bezier(0.16, 1, 0.3, 1) forwards; - transform-origin: left bottom; -} - -#anomalous-toolbox-modal::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; -} - -.anomalous-toolbox-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: repeat(3, 44px); - grid-auto-rows: 44px; - gap: 4px; - margin: 3px 0 1px; - align-content: start; - flex: 1; -} - -.anomalous-toolbox-tile { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: 5px 2px 4px; - min-height: 40px; - box-sizing: border-box; - background: transparent; - border: 1px solid transparent; - border-radius: 6px; - cursor: pointer; - position: relative; - user-select: none; - transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1); -} - -.anomalous-toolbox-tile:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.12); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); -} - -.anomalous-toolbox-tile:active { - transform: scale(0.96); - background: rgba(255, 255, 255, 0.12); -} - -.anomalous-toolbox-tile-icon { - font-size: 13.5px; - line-height: 1.1; - margin-bottom: 2px; - display: flex; - align-items: center; - justify-content: center; - user-select: none; -} - -.anomalous-toolbox-tile-label { - font-size: 9.5px; - font-weight: 500; - color: #cbd5e1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - line-height: 1.15; -} - -.anomalous-toolbox-tile-dot { - position: absolute; - top: 3px; - right: 3px; - width: 4px; - height: 4px; - border-radius: 50%; - background: #f59e0b; - box-shadow: 0 0 4px rgba(245, 158, 11, 0.6); -} - -.anomalous-toolbox-tile-dot.is-ready { - background: #10b981; - box-shadow: 0 0 5px rgba(16, 185, 129, 0.75); -} - /* Suppress all tooltips and action labels on clicked/active elements so they never block vision or interaction */ button:active::before, button:active::after, .anomalous-tooltip-target:active::before, -.anomalous-tooltip-target:active::after, -.anomalous-toolbox-tile:active::before, -.anomalous-toolbox-tile:active::after { +.anomalous-tooltip-target:active::after { display: none !important; opacity: 0 !important; visibility: hidden !important; @@ -367,7 +244,6 @@ button:active::after, background: rgba(255, 255, 255, 0.08) !important; } - #anomalous-detail { flex: 1; display: flex; @@ -422,7 +298,6 @@ button:active::after, user-select: none; } - #anomalous-api-key { background: #333; border: 1px solid #555; @@ -556,4 +431,3 @@ button:active::after, flex: 1; } - diff --git a/web/styles/17-model-types.css b/web/styles/17-model-types.css index aa8b350..2e662ff 100644 --- a/web/styles/17-model-types.css +++ b/web/styles/17-model-types.css @@ -94,3 +94,8 @@ .anomalous-source-sep { opacity: 0.5; } + +/* The Model Source Hub's entry, at the end of the chip row. */ +.anomalous-model-type-chip.is-tool { + margin-left: auto; +}