fix(ui): visual correction audit - return to gothic vampire aesthetics, sweep emojis, center header, and fix tooltip overlap

This commit is contained in:
DemonGatanjieu
2026-09-10 19:06:44 +08:00
parent b72a7a5a5f
commit fc40dead3d
15 changed files with 679 additions and 495 deletions
+7
View File
@@ -178,6 +178,13 @@ DOM or live LiteGraph state.
`recipe_actions.js` own pure or transactional recipe behavior.
- `ui_doctor.js`, `model_picker.js`, and `graph_splice.js` own assistant and
explicit graph-edit behavior.
- Visual Correction Audit & Gothic Vampire Re-alignment (视觉纠偏与哥特美学回归):
- Purged all 83 high-saturation cyberpunk cyan/blue instances (`#38bdf8`, `#00ffcc`, `#2dd4bf`, `#5eead4`, `#007bff`, `#0ea5e9`, `#06b6d4`) and 140+ `rgba(...)` glow values, re-skinning to an aristocratic Crimson (`#dc143c`), Deep Wine (`#722f37`), and Abyssal Violet (`#8b5cf6`) palette;
- Restrained all oversized diffused glows (`box-shadow` / `filter: drop-shadow`) to tight 4~8px interior magic micro-auras, adhering to vampire gothic nobility;
- Swept legacy system emojis across all 13 frontend modules and `locales.js`, replacing them with pure-code vector SVG icons (`3D Cube`, `Network`, `Magic Wand`, `Folder`, `Image Frame`, `Sliders`, `Trash`, etc.);
- Overhauled `#anomalous-header` into a centered symmetric three-column architecture (`leftGroup`, `centerGroup`, `rightGroup`), eliminating left-side visual voids/black holes;
- Injected gothic diamond lattice textures and dark crimson radial auras into `#anomalous-gallery-panel`, `.anomalous-gallery-grid`, `.anomalous-gallery-container`, and `#anomalous-content`;
- Fixed tooltip overlap hazard by re-aligning bottom toolbar tooltip triggers to center-top (`data-tooltip-pos="top"`), completely preventing horizontal button occlusion.
- `locales.js` is the canonical runtime string catalog; `safe_dom.js` is the
trusted rich-text boundary.
- `web/main.js` and `web/modules/ui_sidebar.js` own theme state management and interactive easter-egg domain toggles,
+1 -1
View File
@@ -705,7 +705,7 @@ function applyFloatingTriggerPresentation() {
const icon = document.createElement('span');
icon.className = 'anomalous-trigger-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = '📦';
icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;"><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';
+74 -74
View File
@@ -102,7 +102,7 @@ export const i18n = {
recipeViewGrid: "网格视图",
recipeViewList: "列表视图",
recipeDragHint: "按住拖拽至画布打开新工作流",
recipeDragTargetCanvas: "🚀 释放以在画布打开新工作流",
recipeDragTargetCanvas: "释放以在画布打开新工作流",
recipeStatusReady: "全部就绪",
recipeStatusUnverified: "待校验",
recipeStatusMissing: "模型缺失",
@@ -164,12 +164,12 @@ export const i18n = {
materialChars: "{count} 字符",
materialCopyPromptText: "复制提示词",
notebookSaveError: '保存失败,修改仍留在编辑器中,请重试。',
title: '📦 Anomalous 模型浏览器',
title: 'Anomalous 模型浏览器',
scan: '🔄 扫描目录并同步 C 站数据',
scanTitle: '扫描目录',
scanConfirm: '注意:\n扫描过程将会自动比对 Civitai 并重命名本地模型,同时清理损坏文件。\n确定要开始扫描吗?',
scanning: '扫描中...',
scanBg: '🚀 扫描后台启动!',
scanBg: '扫描后台启动!',
scanDone: '完成',
scanCompleteMsg: '✅ 扫描完成!\n\n⚠️ 提示:由于部分模型已被重命名或去重,请点击 ComfyUI 的 [Refresh] 按钮以同步最新的模型列表。',
eco: '🔋 视频悬浮播放 (节能模式)',
@@ -190,7 +190,7 @@ export const i18n = {
noPreview: '暂无预览图',
clickScan: '点击扫描',
back: '⬅️ 返回',
delModel: '🗑️ 删除',
delModel: '删除',
delConfirm: '确定彻底删除',
delConfirm2: '及其所有关联配置、预览图和缓存文件吗?此操作不可逆!',
delSuccess: '✅ 删除成功!\n',
@@ -204,7 +204,7 @@ export const i18n = {
loading: '加载中…',
loadingCompatible: '加载中...',
backToPrev: '🔙 返回上一层',
applyToCanvas: '➕ 插入节点',
applyToCanvas: '插入节点',
applySuccess: '✅ 已添加至工作流',
help: '帮助',
helpTitle: '📖 使用手册',
@@ -215,12 +215,12 @@ export const i18n = {
<p><strong>1. 🔄 前置准备 (扫描向导)</strong><br>具体位置:界面左侧底部的 <strong>扫描向导 (🔄)</strong>。<br>这是最重要的前置操作!请先打开扫描向导执行扫描。切记保持网络畅通。部分 C站限制级模型信息需要在设置中填入 API-KEY。扫描完成后,插件的各项功能基本激活。</p>
<p><strong>2. 🩺 拯救爆红 (模型医生)</strong><br>具体位置:左侧底部第二个 <strong>听诊器 (🩺)</strong> 按钮。<br>当导入别人使用该插件输出的工作流或者图片时,若模型路径爆红,点击模型医生就能智能识别当前所有节点实现正确路径的替换。</p>
<p><strong>3. 🤖 选中交互与参数预设 (节点助手)</strong><br>具体位置:左侧第三个 <strong>机器人 (🤖)</strong> 按钮。<br>在画布上选中节点后,“动作”页可以可视化替换模型,或在兼容的 MODEL + CLIP 链前后插入 LoRA。“参数方案”页会按配方分组读取工作流配方保存的同类型节点参数;点击一条记录即可把参数应用到当前选中节点。例如选中 KSampler 后,可一键载入配方中的采样器、调度器、步数、CFG 与降噪等值;种子等每次运行变化的参数会被忽略。</p>
<p><strong>4. 📦 工作流配方、参数方案与素材库</strong><br>具体位置:打开 <strong>创作工作台</strong>。<br>配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入/导出配方包。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。</p>
<p><strong>5. ⚙️ 个性化配置 (设置面板)</strong><br>具体位置:左侧底部的 <strong>齿轮 (⚙️)</strong> 按钮。<br>可以调节语言、主页面字体大小;进入“模型设置”可选择视频封面始终播放或悬停播放,以及卡片使用流畅缩略图或原始封面。详情页始终保留原图画质。</p>
<p><strong>6. 📑 顶部导航与提示词笔记</strong><br>具体位置:右侧顶部的按钮分别为 <strong>模型 (📦)</strong>、<strong>图库 (🖼️)</strong>、<strong>创作工作台 (📑)</strong>、<strong>停靠侧边栏 (◧)</strong>。<br>进入创作工作台的“提示词笔记”,点击新建 <strong>➕</strong>。选择基础模型、主模型和 LoRA 后,可通过 <strong>📝纯文本/粘贴</strong> 填入提示词并进行双语对照、分块、查找替换和标签编辑,最后点击 <strong>🚀 发送到画布</strong> 一键组装节点。</p>
<p><strong>7. 📁 文件夹管理 (Folder Manager)</strong><br>具体位置:设置 (⚙️) 面板内的 <strong>📁 文件夹管理</strong>。<br>可以用来隐藏不需要的内置文件夹。隐藏后的文件夹不仅从侧边栏消失,后端的静默扫描和本地读取也会完全跳过,实现真正的“零性能损耗”。并且可以在这里拖拽调整文件夹在侧边栏显示的顺序。</p>
<p><strong>8. 📥 工作流导出与导入 (分享码)</strong><br>具体位置:设置 (⚙️) 按钮左侧的 <strong>导入/导出 (📥)</strong> 按钮。<br>该功能可以将当前的工作流一键编码导出为一段专属分享码,方便发到社交平台。别人只需复制这串分享码,在此界面粘贴即可直接还原导入完整的工作流,并且在导入前系统会自动校验模型是否在本地存在。</p>
<p><strong>9. 💬 提交反馈 / 报告问题</strong><br>具体位置:设置 (⚙️) 面板内的 <strong>💬 提交反馈 / 报告问题</strong>。<br>当你遇到插件报错或有新功能建议时,点击该按钮可一键直达 GitHub Issues 页面提交反馈。</p>
<p><strong>4. 工作流配方、参数方案与素材库</strong><br>具体位置:打开 <strong>创作工作台</strong>。<br>配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入/导出配方包。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。</p>
<p><strong>5. 个性化配置 (设置面板)</strong><br>具体位置:左侧底部的 <strong>齿轮 ()</strong> 按钮。<br>可以调节语言、主页面字体大小;进入“模型设置”可选择视频封面始终播放或悬停播放,以及卡片使用流畅缩略图或原始封面。详情页始终保留原图画质。</p>
<p><strong>6. 📑 顶部导航与提示词笔记</strong><br>具体位置:右侧顶部的按钮分别为 <strong>模型 ()</strong>、<strong>图库 ()</strong>、<strong>创作工作台 (📑)</strong>、<strong>停靠侧边栏 (◧)</strong>。<br>进入创作工作台的“提示词笔记”,点击新建 <strong></strong>。选择基础模型、主模型和 LoRA 后,可通过 <strong>📝纯文本/粘贴</strong> 填入提示词并进行双语对照、分块、查找替换和标签编辑,最后点击 <strong>发送到画布</strong> 一键组装节点。</p>
<p><strong>7. 文件夹管理 (Folder Manager)</strong><br>具体位置:设置 () 面板内的 <strong>文件夹管理</strong>。<br>可以用来隐藏不需要的内置文件夹。隐藏后的文件夹不仅从侧边栏消失,后端的静默扫描和本地读取也会完全跳过,实现真正的“零性能损耗”。并且可以在这里拖拽调整文件夹在侧边栏显示的顺序。</p>
<p><strong>8. 工作流导出与导入 (分享码)</strong><br>具体位置:设置 () 按钮左侧的 <strong>导入/导出 ()</strong> 按钮。<br>该功能可以将当前的工作流一键编码导出为一段专属分享码,方便发到社交平台。别人只需复制这串分享码,在此界面粘贴即可直接还原导入完整的工作流,并且在导入前系统会自动校验模型是否在本地存在。</p>
<p><strong>9. 💬 提交反馈 / 报告问题</strong><br>具体位置:设置 () 面板内的 <strong>💬 提交反馈 / 报告问题</strong>。<br>当你遇到插件报错或有新功能建议时,点击该按钮可一键直达 GitHub Issues 页面提交反馈。</p>
</div>`,
notebooks: '笔记本',
notebookTitle: '笔记本管理',
@@ -252,7 +252,7 @@ export const i18n = {
materialImageDetail: '图片参数详情',
workbenchFullscreen: '全屏工作台',
workbenchWindowed: '窗口模式',
workbenchLoadToCanvas: '⚡ 加载到画布',
workbenchLoadToCanvas: '加载到画布',
workbenchLoadSuccess: '工作流已成功加载到 ComfyUI 画布!',
workbenchLoadFailed: '无法加载工作流到画布:',
workbenchNext: '下一张 (→)',
@@ -293,11 +293,11 @@ export const i18n = {
materialDenoise: '降噪',
materialScheduler: '调度',
materialReferencedModels: '引用的模型清单 ({count})',
materialModelBase: '🎯 主模型 / UNet',
materialModelBase: '主模型 / UNet',
materialModelLora: '🎨 LoRA 微调层',
materialModelVae: '🖼️ VAE 编码器',
materialModelVae: 'VAE 编码器',
materialModelClip: '👁️ 文本编码器 (CLIP)',
materialModelOther: '⚡ 其它模型组件',
materialModelOther: '其它模型组件',
materialLoraStrength: '权重',
materialModelCountLora: '{count} LoRA',
materialModelCountClip: '{count} CLIP',
@@ -369,11 +369,11 @@ export const i18n = {
materialSourceRecipeMissing: '来源已中断 · 配方已删除:{name}',
createNotebook: '新建',
saveNotebook: '保存笔记',
deleteNotebook: '🗑️ 删除',
renameNotebook: '✏️ 重命名',
deleteNotebook: '删除',
renameNotebook: '重命名',
baseModel: 'Base Model (基础模型)',
mainModel: 'Main Model (主模型)',
sendToCanvas: '🚀 发送到画布',
sendToCanvas: '发送到画布',
translate: '翻译',
newNotebookName: '新笔记本名称',
noBaseModel: '请先选择 Base Model',
@@ -408,10 +408,10 @@ export const i18n = {
detailBackDoctor: '⬅ 返回医生面板',
detailBackAssistant: '⬅ 返回助手面板',
detailJumpToBottom: '跳到底部',
detailEdit: '⚙️ 编辑',
detailEdit: '编辑',
detailNotesTitle: '📓 专属备注 (Notes)',
detailEditNotes: '编辑备注',
detailGeneratedGallery: '🖼️ 查看历史生成作品',
detailGeneratedGallery: '查看历史生成作品',
detailTrainedWords: '训练词:',
detailDescription: '描述:',
detailNotes: '备注:',
@@ -525,7 +525,7 @@ export const i18n = {
assistantPreviewFailed: '无法加载预览',
betaFeature: '测试功能',
assistantBetaNotice: '“参数方案”联动仍在测试。应用前请保存当前工作流;它会修改选中节点的参数,但会忽略种子等运行时变化值。“动作”页不属于测试范围。',
assistantHistoryTitle: '🖼️ 历史生成图 ({count})',
assistantHistoryTitle: '历史生成图 ({count})',
assistantViewAll: '查看全部 →',
assistantRestoreWorkflow: '点击恢复此工作流',
assistantViewGallery: '点击查看完整图库',
@@ -534,7 +534,7 @@ export const i18n = {
pickerInsertBeforeTitle: '⬅ 在节点前方插入 LoRA',
pickerInsertAfterTitle: '在节点后方插入 LoRA ➡',
pickerChangeTitle: '🔀 更换当前模型',
pickerFolders: '📁 文件夹',
pickerFolders: '文件夹',
pickerSearch: '🔍 搜索名称或完整路径…',
pickerMainAll: '主模型:全部类型',
pickerNameAsc: '名称 A–Z',
@@ -569,11 +569,11 @@ export const i18n = {
pickerNoMatches: '没有符合条件的模型。',
pickerOperationFailed: '操作失败:',
notebookSelectMain: '请先选择一个主模型。',
mainPreflightTitle: '📥 导入工作流 (预检海关)',
mainPreflightTitle: '导入工作流 (预检海关)',
mainPreflightDesc: '请在此处粘贴工作流的 JSON 代码(未来将支持分享码)。我们将为您提取并校验所有模型依赖。',
mainCancel: '取消',
mainAnalyzePreflight: '🔍 解析并预检',
mainForceLoad: '🚀 强制载入画布',
mainForceLoad: '强制载入画布',
mainNoDependencies: '✅ 未检测到任何模型依赖。',
mainDetectedModels: '扫描到 {count} 个模型资源:',
mainDownloadCivitai: '去 C站下载',
@@ -615,14 +615,14 @@ export const i18n = {
mainResetFloatingTriggerPosition: '重置悬浮入口位置',
mainRetryInit: '插件界面初始化失败,点击可重试',
mainRetryInitEn: 'The plugin UI failed to initialize. Click to retry.',
mainExportTitle: '📦 导出工作流分享码',
mainExportTitle: '导出工作流分享码',
mainSkeletonOption: '精简版 (推荐 B站评论区, 体积小, 丢失坐标)',
mainFullOption: '完整版 (保留所有坐标与颜色, 较长)',
mainGenerate: '生成分享码',
mainCopyClipboard: '复制到剪贴板',
mainClose: '关闭',
mainCopied: '✅ 复制成功!',
mainImportTitle: '📥 导入工作流分享码',
mainImportTitle: '导入工作流分享码',
mainSharePlaceholder: '粘贴 AMB0- 或 AMB1- 开头的分享码...',
mainImportLoad: '导入并加载',
mainShareEmpty: '❌ 分享码为空',
@@ -630,17 +630,17 @@ export const i18n = {
mainDecodeFailed: '❌ 解析失败: ',
mainUnifiedTitle: '🔄 工作流分享与导入',
mainExportWorkflow: '📤 导出当前工作流为分享码',
mainImportWorkflow: '📥 从分享码导入工作流',
mainImportWorkflow: '从分享码导入工作流',
sidebarToggle: '收起/展开侧边栏',
sidebarDoctor: '模型医生',
sidebarAssistant: '节点助手',
sidebarPreflight: '📥 预检导入工作流',
sidebarPreflight: '预检导入工作流',
sidebarSettings: '设置中心',
sidebarScanWizard: '📡 模型扫描向导',
sidebarScanComplete: '✅ 扫描/重命名已完成!数据已为您更新。',
sidebarScanWizardTitle: '扫描向导',
sidebarGlobalScanWizard: '全局扫描向导',
sidebarSingleModelScan: '🎯 单模型扫描向导: {files}',
sidebarSingleModelScan: '单模型扫描向导: {files}',
sidebarGlobalWizardTitle: '全局扫描向导',
sidebarApiKey: 'API 密钥',
sidebarApiKeyPrompt: '请输入 Civitai API Key(留空将清除):',
@@ -680,12 +680,12 @@ export const i18n = {
sidebarExecute: '执行扫描',
sidebarSwitchToEnglish: '🌐 Language: EN',
sidebarSwitchToChinese: '🌐 语言: 中文',
sidebarImportExport: '📥 导入 / 导出工作流 (分享码)',
sidebarImportExport: '导入 / 导出工作流 (分享码)',
sidebarGlobalQuickScan: '🌍 一键全盘极速扫描 (不下载封面/不改名)',
sidebarGlobalQuickScanShort: '🌍 一键全盘扫描 (不下载封面/不改名)',
sidebarGlobalQuickConfirm: '即将执行极速全盘扫描并同步Hash,此操作不改名也不下载封面,是否继续?',
sidebarScanning: '⏳ 扫描中...',
sidebarGlobalStarted: '🚀 全局扫描已后台启动!',
sidebarGlobalStarted: '全局扫描已后台启动!',
sidebarScanDone: '✅ 扫描完成',
scanProgressTitle: '模型扫描进度',
scanProgressPreparing: '正在准备扫描目录…',
@@ -712,9 +712,9 @@ export const i18n = {
sidebarResetConfirm: '确认重置窗口位置、缩放和停靠状态吗?',
sidebarInjectHash: '🟢 注入工作流哈希',
sidebarSkipHash: '⚪ 不注入工作流哈希',
sidebarManageFolders: '📁 文件夹管理',
sidebarManageFolders: '文件夹管理',
sidebarFeedback: '💬 提交反馈 / 报告问题',
sidebarModelSettings: '🖼️ 模型设置',
sidebarModelSettings: '模型设置',
sidebarModelCardSettings: '模型卡片设置',
sidebarModelCardDescription: '只调整模型浏览器里的卡片显示,不会修改模型文件或原始封面。',
sidebarVideoPlayback: '视频封面播放',
@@ -731,7 +731,7 @@ export const i18n = {
sidebarExpandAll: '展开全部',
sidebarCollapseAll: '收起全部',
sidebarSearchModels: '搜索模型...',
sidebarFolderManagerTitle: '📁 侧边栏文件夹管理',
sidebarFolderManagerTitle: '侧边栏文件夹管理',
sidebarFolderManagerDesc: '上下拖拽调整侧边栏的显示顺序。点击眼睛图标可隐藏不需要显示的底层文件夹。隐藏的文件夹不会被扫描,完全不占用性能。',
sidebarCategoryMode: '分类模式 (ComfyUI)',
sidebarPhysicalMode: '物理模式 (本地文件夹)',
@@ -756,7 +756,7 @@ export const i18n = {
settingsBtn: '设置',
closeSettings: '✖ 关闭设置面板',
doctorBtn: '节点助手',
importBtn: '📥 导入 / 导出工作流 (分享码)',
importBtn: '导入 / 导出工作流 (分享码)',
recipeTitle: '工作流配方',
recipeBetaNotice: '工作流配方仍在测试。更新插件以及导入、恢复、删除或应用参数前,请保存当前画布并备份用户 workflows 目录中的 anomalous_recipes 与 anomalous_parameters。',
recipeClose: '关闭工作流配方',
@@ -938,7 +938,7 @@ export const i18n = {
recipeDetailSampler: '采样器',
recipeDetailResolution: '分辨率',
recipeDetailUnavailable: '不可用',
recipeDetailEditOrigin: '✏️ 编辑来源',
recipeDetailEditOrigin: '编辑来源',
recipeOriginDialogTitle: '编辑模型来源信息',
recipeOriginOfficialName: '官方模型名称',
recipeOriginModelUrl: '官方网址 (Model URL)',
@@ -1046,7 +1046,7 @@ export const i18n = {
recipeStatusMissing: '🔴 缺少模型 ({count})',
recipeStatusChecking: '⏳ 检查环境中…',
recipeQuickSpecs: '核心规格',
recipeApplyAllMatches: '⚡ 一键应用全部匹配',
recipeApplyAllMatches: '一键应用全部匹配',
recipeApplyingAllMatches: '正在应用全部匹配…',
recipeApplyAllMatchesSuccess: '✅ 已成功应用全部匹配!',
recipeRawNodesToggle: '节点参数',
@@ -1162,7 +1162,7 @@ export const i18n = {
recipeViewGrid: "Grid View",
recipeViewList: "List View",
recipeDragHint: "Hold & drag onto canvas to load workflow",
recipeDragTargetCanvas: "🚀 Drop to load workflow in canvas",
recipeDragTargetCanvas: "Drop to load workflow in canvas",
recipeStatusReady: "Ready",
recipeStatusUnverified: "Unverified",
recipeStatusMissing: "Missing",
@@ -1224,12 +1224,12 @@ export const i18n = {
materialChars: "{count} characters",
materialCopyPromptText: "Copy prompt",
notebookSaveError: 'Save failed. Your edits remain in the editor; please retry.',
title: '📦 Anomalous Model Browser',
title: 'Anomalous Model Browser',
scan: '🔄 Scan folder and sync Civitai data',
scanTitle: 'Scan Folder',
scanConfirm: 'Notice:\nThe scan process will automatically compare with Civitai and rename your local files.\nStart scan?',
scanning: 'Scanning...',
scanBg: '🚀 Scan started in background!',
scanBg: 'Scan started in background!',
scanDone: 'Done',
scanCompleteMsg: '✅ Scan Complete!\n\n⚠️ Note: Please click [Refresh] to sync the model list since files were renamed.',
eco: '🔋 Eco Mode (Play video on hover)',
@@ -1250,7 +1250,7 @@ export const i18n = {
noPreview: 'No preview available',
clickScan: 'Click Scan',
back: '⬅️ Back',
delModel: '🗑️ Delete',
delModel: 'Delete',
delConfirm: 'Are you sure you want to permanently delete',
delConfirm2: ' and all its associated configs, previews, and cache files? This action is irreversible!',
delSuccess: '✅ Delete Complete!\n',
@@ -1264,7 +1264,7 @@ export const i18n = {
loading: 'Loading…',
loadingCompatible: 'Loading...',
backToPrev: '🔙 Back to prev',
applyToCanvas: '➕ Add Node',
applyToCanvas: 'Add Node',
applySuccess: '✅ Added to workflow',
help: 'Help',
helpTitle: '📖 User Manual',
@@ -1275,12 +1275,12 @@ export const i18n = {
<p><strong>1. 🔄 Prerequisites (Scan Wizard)</strong><br>Location: The <strong>Scan (🔄)</strong> icon at the bottom left.<br>This is the most crucial step! Open the Scan Wizard and execute a scan. Keep your network connected. (API-KEY may be needed in Settings for NSFW Civitai models). Scanning activates all core features.</p>
<p><strong>2. 🩺 Fix Broken Nodes (Model Doctor)</strong><br>Location: The 2nd icon <strong>(Stethoscope 🩺)</strong> from the bottom left.<br>When importing a workflow/image exported by this plugin and nodes turn red, open the Model Doctor to intelligently identify and replace them with correct local paths.</p>
<p><strong>3. 🤖 Node actions and parameter presets (Node Assistant)</strong><br>Location: The 3rd icon <strong>(Robot 🤖)</strong> from the bottom left.<br>After selecting a canvas node, Actions can visually replace its model or insert a LoRA before/after a compatible MODEL + CLIP chain. Parameter Presets reads same-type node values saved by Workflow Recipes and groups them by recipe. Click one record to apply it to the selected node. For example, selecting a KSampler can load its sampler, scheduler, steps, CFG, and denoise values in one click; volatile values such as seeds are ignored.</p>
<p><strong>4. 📦 Workflow Recipes and Parameter Sets</strong><br>Location: Open the <strong>Creative Workspace</strong> and switch to <strong>Workflow Recipes</strong>.<br>A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both can be shared as import/export packages. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.</p>
<p><strong>5. ⚙️ Settings Panel</strong><br>Location: The <strong>Gear (⚙️)</strong> icon at the bottom left.<br>Adjust language and UI scale. Open “Model Settings” to choose always-play or hover-play video covers and optimized thumbnails or original covers. Detail pages always retain original image quality.</p>
<p><strong>6. 📑 Top Navigation & Prompt Notes</strong><br>Location: Top-right tabs <strong>Models (📦)</strong>, <strong>Gallery (🖼️)</strong>, <strong>Creative Workspace (📑)</strong>, and <strong>Dock (◧)</strong>.<br>Open Prompt Notes in the Creative Workspace and click <strong>➕</strong>. Select a Base Model, Main Model, and LoRAs; use <strong>📝 Edit Raw/Paste</strong> for bilingual prompts, chunking, find/replace, and tag editing; then click <strong>🚀 Send to Canvas</strong> to assemble the nodes.</p>
<p><strong>7. 📁 Folder Manager</strong><br>Location: Inside the Settings (⚙️) panel, click <strong>📁 Manage Folders</strong>.<br>Use this to hide unneeded built-in folders. Hidden folders will be completely skipped by background scans and I/O reads, achieving true "zero performance cost". You can also drag and drop to reorder how folders appear in the sidebar.</p>
<p><strong>8. 📥 Workflow Export & Import (Share Code)</strong><br>Location: The <strong>Import/Export (📥)</strong> icon to the left of the Settings gear.<br>You can encode your current workflow into a single share code for easy sharing on social platforms. Others can simply copy this code and paste it here to instantly import the workflow. The system will also automatically pre-check if the required models exist locally.</p>
<p><strong>9. 💬 Submit Feedback / Report Bug</strong><br>Location: Inside the Settings (⚙️) panel, click <strong>💬 Submit Feedback / Report Bug</strong>.<br>If you encounter bugs or have feature requests, click this to go directly to our GitHub Issues page.</p>
<p><strong>4. Workflow Recipes and Parameter Sets</strong><br>Location: Open the <strong>Creative Workspace</strong> and switch to <strong>Workflow Recipes</strong>.<br>A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both can be shared as import/export packages. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.</p>
<p><strong>5. Settings Panel</strong><br>Location: The <strong>Gear ()</strong> icon at the bottom left.<br>Adjust language and UI scale. Open “Model Settings” to choose always-play or hover-play video covers and optimized thumbnails or original covers. Detail pages always retain original image quality.</p>
<p><strong>6. 📑 Top Navigation & Prompt Notes</strong><br>Location: Top-right tabs <strong>Models ()</strong>, <strong>Gallery ()</strong>, <strong>Creative Workspace (📑)</strong>, and <strong>Dock (◧)</strong>.<br>Open Prompt Notes in the Creative Workspace and click <strong></strong>. Select a Base Model, Main Model, and LoRAs; use <strong>📝 Edit Raw/Paste</strong> for bilingual prompts, chunking, find/replace, and tag editing; then click <strong>Send to Canvas</strong> to assemble the nodes.</p>
<p><strong>7. Folder Manager</strong><br>Location: Inside the Settings () panel, click <strong>Manage Folders</strong>.<br>Use this to hide unneeded built-in folders. Hidden folders will be completely skipped by background scans and I/O reads, achieving true "zero performance cost". You can also drag and drop to reorder how folders appear in the sidebar.</p>
<p><strong>8. Workflow Export & Import (Share Code)</strong><br>Location: The <strong>Import/Export ()</strong> icon to the left of the Settings gear.<br>You can encode your current workflow into a single share code for easy sharing on social platforms. Others can simply copy this code and paste it here to instantly import the workflow. The system will also automatically pre-check if the required models exist locally.</p>
<p><strong>9. 💬 Submit Feedback / Report Bug</strong><br>Location: Inside the Settings () panel, click <strong>💬 Submit Feedback / Report Bug</strong>.<br>If you encounter bugs or have feature requests, click this to go directly to our GitHub Issues page.</p>
</div>`,
notebooks: 'Notebooks',
notebookTitle: 'Notebook Manager',
@@ -1312,7 +1312,7 @@ export const i18n = {
materialImageDetail: 'Image Parameter Details',
workbenchFullscreen: 'Fullscreen Workbench',
workbenchWindowed: 'Windowed Mode',
workbenchLoadToCanvas: '⚡ Load to Canvas',
workbenchLoadToCanvas: 'Load to Canvas',
workbenchLoadSuccess: 'Workflow loaded to ComfyUI canvas successfully!',
workbenchLoadFailed: 'Could not load workflow into canvas: ',
workbenchNext: 'Next (→)',
@@ -1353,11 +1353,11 @@ export const i18n = {
materialDenoise: 'Denoise',
materialScheduler: 'Scheduler',
materialReferencedModels: 'Referenced Models ({count})',
materialModelBase: '🎯 Base Model / UNet',
materialModelBase: 'Base Model / UNet',
materialModelLora: '🎨 LoRA Layers',
materialModelVae: '🖼️ VAE',
materialModelVae: 'VAE',
materialModelClip: '👁️ Text Encoder (CLIP)',
materialModelOther: '⚡ Other Models',
materialModelOther: 'Other Models',
materialLoraStrength: 'Weight',
materialModelCountLora: '{count} LoRA',
materialModelCountClip: '{count} CLIP',
@@ -1429,11 +1429,11 @@ export const i18n = {
materialSourceRecipeMissing: 'Source broken · recipe deleted: {name}',
createNotebook: 'New',
saveNotebook: '💾 Save prompt note',
deleteNotebook: '🗑️ Delete',
renameNotebook: '✏️ Rename',
deleteNotebook: 'Delete',
renameNotebook: 'Rename',
baseModel: 'Base Model',
mainModel: 'Main Model',
sendToCanvas: '🚀 Send to Canvas',
sendToCanvas: 'Send to Canvas',
translate: 'Translate',
newNotebookName: 'New Notebook Name',
noBaseModel: 'Select a Base Model first',
@@ -1468,10 +1468,10 @@ export const i18n = {
detailBackDoctor: '⬅ Back to Doctor',
detailBackAssistant: '⬅ Back to Assistant',
detailJumpToBottom: 'Jump to bottom',
detailEdit: '⚙️ Edit',
detailEdit: 'Edit',
detailNotesTitle: '📓 Custom Notes',
detailEditNotes: 'Edit Notes',
detailGeneratedGallery: '🖼️ View Generated Gallery',
detailGeneratedGallery: 'View Generated Gallery',
detailTrainedWords: 'Trained Words:',
detailDescription: 'Description:',
detailNotes: 'Notes:',
@@ -1585,7 +1585,7 @@ export const i18n = {
assistantPreviewFailed: 'Failed to load preview',
betaFeature: 'Beta feature',
assistantBetaNotice: 'The Parameter Set integration is still in beta. Save the current workflow before applying a preset; it changes the selected node while ignoring volatile values such as seeds. The Actions tab is not part of this beta.',
assistantHistoryTitle: '🖼️ History ({count})',
assistantHistoryTitle: 'History ({count})',
assistantViewAll: 'View All →',
assistantRestoreWorkflow: 'Click to restore workflow',
assistantViewGallery: 'Click to view full gallery',
@@ -1594,7 +1594,7 @@ export const i18n = {
pickerInsertBeforeTitle: '⬅ Insert LoRA Before Node',
pickerInsertAfterTitle: 'Insert LoRA After Node ➡',
pickerChangeTitle: '🔀 Change Current Model',
pickerFolders: '📁 Folders',
pickerFolders: 'Folders',
pickerSearch: '🔍 Search name or full path…',
pickerMainAll: 'Main model: All types',
pickerNameAsc: 'Name A–Z',
@@ -1629,11 +1629,11 @@ export const i18n = {
pickerNoMatches: 'No models match the current filters.',
pickerOperationFailed: 'Operation failed: ',
notebookSelectMain: 'Please select a Main Model first.',
mainPreflightTitle: '📥 Pre-flight Import Workflow',
mainPreflightTitle: 'Pre-flight Import Workflow',
mainPreflightDesc: 'Paste the workflow JSON here. We will extract and validate all model dependencies before loading.',
mainCancel: 'Cancel',
mainAnalyzePreflight: '🔍 Analyze & Pre-flight',
mainForceLoad: '🚀 Force Load to Canvas',
mainForceLoad: 'Force Load to Canvas',
mainNoDependencies: '✅ No model dependencies detected.',
mainDetectedModels: 'Detected {count} model resources:',
mainDownloadCivitai: 'Download',
@@ -1675,14 +1675,14 @@ export const i18n = {
mainResetFloatingTriggerPosition: 'Reset floating entry position',
mainRetryInit: '插件界面初始化失败,点击可重试',
mainRetryInitEn: 'The plugin UI failed to initialize. Click to retry.',
mainExportTitle: '📦 Export Workflow Share Code',
mainExportTitle: 'Export Workflow Share Code',
mainSkeletonOption: 'Skeleton (Compact, best for comments, loses coords)',
mainFullOption: 'Full (Keeps coords and colors, longer string)',
mainGenerate: 'Generate',
mainCopyClipboard: 'Copy',
mainClose: 'Close',
mainCopied: '✅ Copied successfully!',
mainImportTitle: '📥 Import Workflow Share Code',
mainImportTitle: 'Import Workflow Share Code',
mainSharePlaceholder: 'Paste AMB0- or AMB1- share code here...',
mainImportLoad: 'Import & Load',
mainShareEmpty: '❌ Share code is empty',
@@ -1690,17 +1690,17 @@ export const i18n = {
mainDecodeFailed: '❌ Decode Failed: ',
mainUnifiedTitle: '🔄 Workflow Share & Import',
mainExportWorkflow: '📤 Export Workflow to Share Code',
mainImportWorkflow: '📥 Import Workflow from Share Code',
mainImportWorkflow: 'Import Workflow from Share Code',
sidebarToggle: 'Toggle Sidebar',
sidebarDoctor: 'Model Doctor',
sidebarAssistant: 'Node Assistant',
sidebarPreflight: '📥 Preflight Import',
sidebarPreflight: 'Preflight Import',
sidebarSettings: 'Settings Hub',
sidebarScanWizard: '📡 Model Scan Wizard',
sidebarScanComplete: '✅ Scan/Rename completed! Data updated.',
sidebarScanWizardTitle: 'Scan Wizard',
sidebarGlobalScanWizard: 'Global Scan Wizard',
sidebarSingleModelScan: '🎯 Single Model Scan: {files}',
sidebarSingleModelScan: 'Single Model Scan: {files}',
sidebarGlobalWizardTitle: 'Global Scan Wizard',
sidebarApiKey: 'API Key',
sidebarApiKeyPrompt: 'Enter Civitai API Key (leave blank to clear):',
@@ -1740,12 +1740,12 @@ export const i18n = {
sidebarExecute: 'Execute Scan',
sidebarSwitchToEnglish: '🌐 Language: EN',
sidebarSwitchToChinese: '🌐 语言: 中文',
sidebarImportExport: '📥 Import / Export Workflow (Share Code)',
sidebarImportExport: 'Import / Export Workflow (Share Code)',
sidebarGlobalQuickScan: '🌍 Global Quick Scan (No rename/No media)',
sidebarGlobalQuickScanShort: '🌍 Global Quick Scan (No rename/No media)',
sidebarGlobalQuickConfirm: 'Start global quick scan to sync all hashes?',
sidebarScanning: '⏳ Scanning...',
sidebarGlobalStarted: '🚀 Global Scan started in background!',
sidebarGlobalStarted: 'Global Scan started in background!',
sidebarScanDone: '✅ Scan Complete',
scanProgressTitle: 'Model scan progress',
scanProgressPreparing: 'Preparing scan folders…',
@@ -1772,9 +1772,9 @@ export const i18n = {
sidebarResetConfirm: 'Reset window position, scale and dock state?',
sidebarInjectHash: '🟢 Inject Workflow Hash',
sidebarSkipHash: '⚪ Skip Workflow Hash',
sidebarManageFolders: '📁 Manage Folders',
sidebarManageFolders: 'Manage Folders',
sidebarFeedback: '💬 Submit Feedback / Report Bug',
sidebarModelSettings: '🖼️ Model Settings',
sidebarModelSettings: 'Model Settings',
sidebarModelCardSettings: 'Model Card Settings',
sidebarModelCardDescription: 'Only changes cards in the model browser. Model files and original covers remain untouched.',
sidebarVideoPlayback: 'Video cover playback',
@@ -1791,7 +1791,7 @@ export const i18n = {
sidebarExpandAll: 'Expand All',
sidebarCollapseAll: 'Collapse All',
sidebarSearchModels: 'Search models...',
sidebarFolderManagerTitle: '📁 Manage Folders',
sidebarFolderManagerTitle: 'Manage Folders',
sidebarFolderManagerDesc: 'Drag and drop to reorder folders. Click the eye icon to hide unneeded folders. Hidden folders are skipped during scanning and consume zero performance.',
sidebarCategoryMode: 'Category Mode (ComfyUI)',
sidebarPhysicalMode: 'Physical Mode (Local)',
@@ -1816,7 +1816,7 @@ export const i18n = {
settingsBtn: 'Settings',
closeSettings: '✖ Close Settings',
doctorBtn: 'Node Assistant',
importBtn: '📥 Import / Export Workflow (Share Code)',
importBtn: 'Import / Export Workflow (Share Code)',
recipeTitle: 'Workflow Recipes',
recipeBetaNotice: 'Workflow Recipes are still in beta. Before updating the plugin or importing, restoring, deleting, or applying parameters, save the canvas and back up anomalous_recipes and anomalous_parameters under your user workflows directory.',
recipeClose: 'Close Workflow Recipes',
@@ -1998,7 +1998,7 @@ export const i18n = {
recipeDetailSampler: 'Sampler',
recipeDetailResolution: 'Resolution',
recipeDetailUnavailable: 'Unavailable',
recipeDetailEditOrigin: '✏️ Edit Origin',
recipeDetailEditOrigin: 'Edit Origin',
recipeOriginDialogTitle: 'Edit Model Origin',
recipeOriginOfficialName: 'Official Model Name',
recipeOriginModelUrl: 'Official Model URL',
@@ -2106,7 +2106,7 @@ export const i18n = {
recipeStatusMissing: '🔴 Missing Models ({count})',
recipeStatusChecking: '⏳ Checking Environment…',
recipeQuickSpecs: 'Quick Specs',
recipeApplyAllMatches: '⚡ Apply All Matches',
recipeApplyAllMatches: 'Apply All Matches',
recipeApplyingAllMatches: 'Applying all matches…',
recipeApplyAllMatchesSuccess: '✅ Applied all local matches!',
recipeRawNodesToggle: 'Raw Node Parameters',
+5 -5
View File
@@ -485,7 +485,7 @@ export function showDetail(model) {
notesTitle.style.letterSpacing = '0.5px';
const notesEditBtn = document.createElement('button');
notesEditBtn.innerHTML = '✏️';
notesEditBtn.innerHTML = '<svg style="width:13px;height:13px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
notesEditBtn.title = t('detailEditNotes');
notesEditBtn.style.background = 'transparent';
notesEditBtn.style.border = 'none';
@@ -858,7 +858,7 @@ export function showEditModal(model) {
coverRow.style.gap = '8px';
const galleryBtn = document.createElement('button');
galleryBtn.textContent = `🖼️ ${t('detailPickGallery')}`;
galleryBtn.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>${t('detailPickGallery')}`;
galleryBtn.style.padding = '8px';
galleryBtn.style.background = '#303134';
galleryBtn.style.color = '#8ab4f8';
@@ -875,7 +875,7 @@ export function showEditModal(model) {
};
const localBtn = document.createElement('button');
localBtn.textContent = `📁 ${t('detailUploadLocal')}`;
localBtn.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>${t('detailUploadLocal')}`;
localBtn.style.padding = '8px';
localBtn.style.background = '#303134';
localBtn.style.color = '#8ab4f8';
@@ -1641,7 +1641,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
} else {
imgContainer.innerHTML = '<span style="font-size:40px;opacity:0.2;">📄</span>';
imgContainer.innerHTML = '<span style="display:inline-block;opacity:0.25;"><svg style="width:40px;height:40px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></span>';
}
card.appendChild(imgContainer);
@@ -1882,7 +1882,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
leftPart.style.display = 'flex';
leftPart.style.alignItems = 'center';
leftPart.style.gap = '8px';
leftPart.innerHTML = `<span style="color:#9aa0a6">📁</span> <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">${escapeHtml(fData.name)}</span> <span style="color:#5f6368;font-size:12px">(${escapeHtml(fData.model_count)})</span>`;
leftPart.innerHTML = `<span style="display:inline-flex;align-items:center;color:#9aa0a6"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span> <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">${escapeHtml(fData.name)}</span> <span style="color:#5f6368;font-size:12px">(${escapeHtml(fData.model_count)})</span>`;
const badge = document.createElement('div');
badge.className = 'selection-badge';
+25 -12
View File
@@ -1394,8 +1394,11 @@ export function _openGalleryReplacer(node, w, options = {}) {
folders.forEach(folderPath => {
const button = document.createElement('button');
const depth = folderPath ? folderPath.split('/').length - 1 : 0;
const label = folderPath ? folderPath.split('/').pop() : t('pickerAllModels');
button.textContent = `${folderPath ? '📂' : '📦'} ${label} (${folderCounts.get(folderPath) || 0})`;
const label = folderPath ? folderPath.split('/').pop() : t('pickerAllModels');
const folderIcon = folderPath
? '<svg style="width:13px;height:13px;vertical-align:-2px;margin-right:4px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>'
: '<svg style="width:13px;height:13px;vertical-align:-2px;margin-right:4px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
button.innerHTML = `${folderIcon}${escapeHtml(label)} (${folderCounts.get(folderPath) || 0})`;
button.title = folderPath || label;
button.style.cssText = `text-align:left;padding:7px 8px 7px ${8 + depth * 14}px;border-radius:6px;border:none;cursor:pointer;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:${selectedFolder === folderPath ? '#fff' : '#aaa'};background:${selectedFolder === folderPath ? 'rgba(25,118,210,0.45)' : 'transparent'};`;
button.onclick = () => {
@@ -1460,7 +1463,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
image.style.cssText = 'width:100%;height:100%;object-fit:cover;';
previewBox.appendChild(image);
} else {
previewBox.textContent = '🖼️';
previewBox.innerHTML = '<span style="display:inline-block;opacity:0.25;"><svg style="width:28px;height:28px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></span>';
}
if (isCurrent) {
const badge = document.createElement('span');
@@ -1687,6 +1690,23 @@ function renderMaterialPresets(node, container, forceRefresh) {
section.append(header, loader);
container.appendChild(section);
const url = `/anomalous/materials/by_node_type?type=${encodeURIComponent(node.type)}${forceRefresh ? '&refresh=1' : ''}`;
fetch(url, { cache: 'no-store' }).then(response => {
if (!response.ok) throw new Error('material preset request failed');
return response.json();
}).then(payload => {
loader.remove();
const materials = Array.isArray(payload.materials) ? payload.materials : [];
if (!materials.length) {
const empty = document.createElement('div');
empty.style.cssText = 'font-size:11px;color:#666;text-align:center;padding:10px;background:rgba(0,0,0,.2);border-radius:8px;border:1px dashed rgba(255,255,255,.1);';
empty.textContent = t('materialNoNodePresets');
const loader = document.createElement('div');
loader.style.cssText = 'font-size:12px;color:#555;text-align:center;padding:10px;';
loader.textContent = t('loading');
section.append(header, loader);
container.appendChild(section);
const url = `/anomalous/materials/by_node_type?type=${encodeURIComponent(node.type)}${forceRefresh ? '&refresh=1' : ''}`;
fetch(url, { cache: 'no-store' }).then(response => {
if (!response.ok) throw new Error('material preset request failed');
@@ -1705,26 +1725,19 @@ function renderMaterialPresets(node, container, forceRefresh) {
const group = document.createElement('div');
group.style.cssText = 'display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid rgba(92,202,180,.16);border-radius:9px;background:rgba(92,202,180,.045);';
const name = document.createElement('strong');
name.textContent = `🖼️ ${material.name || t('materialUntitled')}`;
name.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>${escapeHtml(material.name || t('materialUntitled'))}`;
name.style.cssText = 'font-size:12px;color:#bcebe1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
group.appendChild(name);
for (const block of (Array.isArray(material.blocks) ? material.blocks : [])) {
const apply = document.createElement('button');
const suffix = material.blocks.length > 1 ? ` #${block.occurrence}` : '';
const label = `✨ ${block.title || block.type}${suffix}`;
const label = `${block.title || block.type}${suffix}`;
apply.textContent = label;
apply.style.cssText = 'padding:8px 10px;border:1px solid rgba(255,255,255,.1);border-radius:6px;background:rgba(255,255,255,.05);color:#fff;text-align:left;cursor:pointer;';
apply.onclick = () => {
apply.disabled = true;
try {
applyMaterialToSelectedNode(node, block, material.workflow_hashes, group);
apply.textContent = `✅ ${t('materialNodeApplied')}`;
apply.style.background = 'rgba(46,139,87,.6)';
} catch (error) {
console.error('[Anomalous] Failed to apply material node parameters:', error);
apply.textContent = `⚠️ ${t('recipeParameterApplyError')}`;
apply.style.background = 'rgba(180,60,60,.55)';
}
window.setTimeout(() => {
apply.disabled = false;
+2 -2
View File
@@ -152,7 +152,7 @@ export async function loadGalleryImages(page = 1, reset = false) {
const delBtn = document.createElement('button');
delBtn.className = 'anomalous-gallery-delete';
delBtn.innerHTML = '🗑️';
delBtn.innerHTML = '<svg style="width:14px;height:14px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
delBtn.title = t('galleryDelete');
delBtn.onclick = (e) => {
@@ -189,7 +189,7 @@ export async function loadGalleryImages(page = 1, reset = false) {
btnRow.style.gap = '12px';
const confirmBtn = document.createElement('button');
confirmBtn.textContent = `🗑️ ${t('galleryDelete')}`;
confirmBtn.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>${t('galleryDelete')}`;
confirmBtn.style.background = '#dc3545';
confirmBtn.style.color = '#fff';
confirmBtn.style.border = 'none';
+10 -10
View File
@@ -673,11 +673,11 @@ function buildModelsSection(orderedRefs, groups, onOpenModel) {
wrap.appendChild(groupEl);
};
renderGroup(t('materialModelBase') || '🎯 主模型 / UNet', groups.base, 'is-base');
renderGroup(t('materialModelLora') || '🎨 LoRA 微调层', groups.lora, 'is-lora');
renderGroup(t('materialModelClip') || '👁️ 文本编码器 (CLIP)', groups.clip, 'is-clip');
renderGroup(t('materialModelVae') || '🖼️ VAE 编码器', groups.vae, 'is-vae');
renderGroup(t('materialModelOther') || '⚡ 其它模型组件', groups.other, 'is-other');
renderGroup(t('materialModelBase') || '主模型 / UNet', groups.base, 'is-base');
renderGroup(t('materialModelLora') || 'LoRA 微调层', groups.lora, 'is-lora');
renderGroup(t('materialModelClip') || '文本编码器 (CLIP)', groups.clip, 'is-clip');
renderGroup(t('materialModelVae') || 'VAE 编码器', groups.vae, 'is-vae');
renderGroup(t('materialModelOther') || '其它模型组件', groups.other, 'is-other');
return wrap;
}
@@ -1075,10 +1075,10 @@ async function renderInspectorContent(data, item) {
tabsBar.className = 'anomalous-workbench-tabs-bar';
const tabs = [
{ id: 'specs', label: `📊 ${t('workbenchTabOverview') || '核心参数'}` },
{ id: 'prompts', label: `💬 ${t('workbenchTabPrompts') || '提示词'}` },
{ id: 'models', label: `🧩 ${t('workbenchTabModels') || '模型与LoRA'}` },
{ id: 'nodes', label: `⚙️ ${t('workbenchTabNodes') || '工作流节点'}` },
{ id: 'specs', label: t('workbenchTabOverview') || '核心参数', icon: '<svg style="width:13px;height:13px;margin-right:5px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>' },
{ id: 'prompts', label: t('workbenchTabPrompts') || '提示词', icon: '<svg style="width:13px;height:13px;margin-right:5px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>' },
{ id: 'models', label: t('workbenchTabModels') || '模型与LoRA', icon: '<svg style="width:13px;height:13px;margin-right:5px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>' },
{ id: 'nodes', label: t('workbenchTabNodes') || '工作流节点', icon: '<svg style="width:13px;height:13px;margin-right:5px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>' },
];
const tabPanels = {};
@@ -1090,7 +1090,7 @@ async function renderInspectorContent(data, item) {
tabBtn.className = (wb.activeTab === tDef.id)
? 'anomalous-workbench-tab-btn is-active'
: 'anomalous-workbench-tab-btn';
tabBtn.textContent = tDef.label;
tabBtn.innerHTML = `${tDef.icon}<span>${tDef.label}</span>`;
tabBtn.onclick = () => {
wb.activeTab = tDef.id;
+3 -3
View File
@@ -159,7 +159,7 @@ export async function loadModels() {
};
const applyBtn = document.createElement('button');
applyBtn.innerHTML = '➕';
applyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`;
applyBtn.className = 'anomalous-card-action-btn anomalous-tooltip-target';
applyBtn.removeAttribute('title');
applyBtn.setAttribute('data-tooltip', t('applyToCanvas'));
@@ -178,7 +178,7 @@ export async function loadModels() {
applyBtn.style.display = 'none';
const singleScanBtn = document.createElement('button');
singleScanBtn.innerHTML = '🎯';
singleScanBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>`;
singleScanBtn.className = 'anomalous-card-action-btn anomalous-tooltip-target';
singleScanBtn.removeAttribute('title');
singleScanBtn.setAttribute('data-tooltip', t('scanModelPrecisely'));
@@ -203,7 +203,7 @@ export async function loadModels() {
card.appendChild(singleScanBtn);
const editBtn = document.createElement('button');
editBtn.innerHTML = '⚙️';
editBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`;
editBtn.className = 'anomalous-card-action-btn anomalous-tooltip-target';
editBtn.removeAttribute('title');
editBtn.setAttribute('data-tooltip', t('editModel'));
+6 -4
View File
@@ -266,8 +266,9 @@ function buildMaterialDetailHeader(owner, material) {
const headerActions = document.createElement('div');
headerActions.className = 'anomalous-library-detail-actions';
if ((material.capabilities || []).includes('open_workflow')) {
const open = text(headerActions, 'button', `🚀 ${t('materialOpenWorkflow')}`, 'anomalous-btn-primary');
const open = text(headerActions, 'button', '', 'anomalous-btn-primary');
open.type = 'button';
open.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>${t('materialOpenWorkflow')}`;
open.onclick = async () => {
open.disabled = true;
try {
@@ -282,8 +283,9 @@ function buildMaterialDetailHeader(owner, material) {
const edit = text(headerActions, 'button', t('materialEditDetails'), 'anomalous-btn-ghost');
edit.type = 'button';
edit.onclick = () => toggleMaterialEditor(owner, material, header);
const remove = text(headerActions, 'button', `🗑️ ${t('materialDelete')}`, 'anomalous-btn-ghost');
const remove = text(headerActions, 'button', '', 'anomalous-btn-ghost');
remove.type = 'button';
remove.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>${t('materialDelete')}`;
remove.onclick = () => deleteMaterial(owner, material);
header.appendChild(headerActions);
@@ -515,7 +517,7 @@ function renderMaterialCard(owner, material) {
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'anomalous-material-card-action-btn anomalous-material-card-delete';
remove.innerHTML = '🗑️';
remove.innerHTML = '<svg style="width:13px;height:13px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
remove.title = t('materialDelete');
remove.onclick = (e) => {
e.stopPropagation();
@@ -529,7 +531,7 @@ function renderMaterialCard(owner, material) {
const quickOpen = document.createElement('button');
quickOpen.type = 'button';
quickOpen.className = 'anomalous-material-card-action-btn anomalous-material-card-quick-load';
quickOpen.innerHTML = '🚀';
quickOpen.innerHTML = '<svg style="width:13px;height:13px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
quickOpen.title = t('materialOpenWorkflow');
quickOpen.onclick = async (e) => {
e.stopPropagation();
+4 -3
View File
@@ -139,7 +139,8 @@ export async function showNotebooks() {
btnRow.style.gap = '5px';
const createBtn = document.createElement('button');
createBtn.innerHTML = `➕ <span class="anomalous-nb-create-text">${t('createNotebook')}</span>`;
const createBtnHtml = `<svg style="width:13px;height:13px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span class="anomalous-nb-create-text">${t('createNotebook')}</span>`;
createBtn.innerHTML = createBtnHtml;
createBtn.className = 'anomalous-btn-primary';
const createInput = document.createElement('input');
@@ -168,7 +169,7 @@ export async function showNotebooks() {
createInput.value = '';
}
createInput.style.display = 'none';
createBtn.innerHTML = `➕ <span class="anomalous-nb-create-text">${t('createNotebook')}</span>`;
createBtn.innerHTML = createBtnHtml;
}
};
@@ -217,7 +218,7 @@ export async function refreshNotebooks(autoOpenFirst = false) {
if (this.currentNotebook && this.currentNotebook.filename === nb.filename) {
item.classList.add('active');
}
item.innerHTML = `<span class="anomalous-nb-item-icon">📄&nbsp;</span><span class="anomalous-nb-item-text">${escapeHtml(nb.name)}</span>`;
item.innerHTML = `<span class="anomalous-nb-item-icon"><svg style="width:13px;height:13px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></span><span class="anomalous-nb-item-text">${escapeHtml(nb.name)}</span>`;
item.onclick = () => {
this.currentNotebook = nb;
this.renderNotebookEditor();
+43 -29
View File
@@ -331,21 +331,24 @@ function buildPromptComposer(owner, container, options = {}) {
const leftPanel = text(workbenchGrid, 'section', '', 'anomalous-workbench-left-panel');
const leftHeader = text(leftPanel, 'div', '', 'anomalous-workbench-col-header');
const leftTitleWrap = text(leftHeader, 'div', '', 'anomalous-workbench-col-title');
leftTitleWrap.innerHTML = `📚 <strong>${window.anomalous_browser_lang === 'zh' ? '成型提示词库' : 'Prompt Library'}</strong> <span class="anomalous-sub-counter"></span>`;
leftTitleWrap.innerHTML = `<strong>${window.anomalous_browser_lang === 'zh' ? '成型提示词库' : 'Prompt Library'}</strong> <span class="anomalous-sub-counter"></span>`;
// Button: Create New Custom Card (placed in header right, never wraps)
const newCardTriggerBtn = text(leftHeader, 'button', `➕ ${window.anomalous_browser_lang === 'zh' ? '新建词卡' : 'New Card'}`, 'anomalous-btn-ghost anomalous-btn-sm');
const newCardTriggerBtn = text(leftHeader, 'button', '', 'anomalous-btn-ghost anomalous-btn-sm');
newCardTriggerBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>${window.anomalous_browser_lang === 'zh' ? '新建词卡' : 'New Card'}`;
newCardTriggerBtn.title = window.anomalous_browser_lang === 'zh' ? '新建并保存一张提示词卡片' : 'Create a new prompt card';
// Sub-action bar: Node extract and Material Library import (full-width, balanced)
const leftSubActions = text(leftPanel, 'div', '', 'anomalous-workbench-sub-actions');
// Button 1: Extract Prompts from Selected Canvas Node
const extractNodeBtn = text(leftSubActions, 'button', `🎯 ${window.anomalous_browser_lang === 'zh' ? '从节点提取' : 'From Node'}`, 'anomalous-btn-primary anomalous-btn-sm anomalous-btn-extract-node');
const extractNodeBtn = text(leftSubActions, 'button', '', 'anomalous-btn-primary anomalous-btn-sm anomalous-btn-extract-node');
extractNodeBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>${window.anomalous_browser_lang === 'zh' ? '从节点提取' : 'From Node'}`;
extractNodeBtn.title = window.anomalous_browser_lang === 'zh' ? '读取 ComfyUI 画布当前选中节点的提示词文本并生成词卡' : 'Extract prompt text from selected canvas node into cards';
// Button 2: One-click Sync / Import from Material Library
const importMaterialsBtn = text(leftSubActions, 'button', `📥 ${window.anomalous_browser_lang === 'zh' ? '从素材库导入' : 'Import from Library'}`, 'anomalous-btn-ghost anomalous-btn-sm');
const importMaterialsBtn = text(leftSubActions, 'button', '', 'anomalous-btn-ghost anomalous-btn-sm');
importMaterialsBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>${window.anomalous_browser_lang === 'zh' ? '从素材库导入' : 'Import from Library'}`;
importMaterialsBtn.title = window.anomalous_browser_lang === 'zh' ? '读取素材库,一键把素材库里沉淀的提示词捞入当前工坊词库' : 'Read and import all prompts from Material Library into workbench';
// Search and category filters bar
@@ -360,10 +363,10 @@ function buildPromptComposer(owner, container, options = {}) {
const leftCategoryPills = text(leftPanel, 'div', '', 'anomalous-workbench-category-pills');
const filterCats = [
{ id: 'all', label: window.anomalous_browser_lang === 'zh' ? '全部' : 'All' },
{ id: 'base', label: window.anomalous_browser_lang === 'zh' ? '💎 通用' : '💎 Base' },
{ id: 'style', label: window.anomalous_browser_lang === 'zh' ? '🎨 风格' : '🎨 Style' },
{ id: 'subject', label: window.anomalous_browser_lang === 'zh' ? '🧍 主体' : '🧍 Subject' },
{ id: 'trigger', label: window.anomalous_browser_lang === 'zh' ? '⚡ 触发' : '⚡ LoRA' },
{ id: 'base', label: window.anomalous_browser_lang === 'zh' ? '通用底模' : 'Base' },
{ id: 'style', label: window.anomalous_browser_lang === 'zh' ? '风格氛围' : 'Style' },
{ id: 'subject', label: window.anomalous_browser_lang === 'zh' ? '主体内容' : 'Subject' },
{ id: 'trigger', label: window.anomalous_browser_lang === 'zh' ? '触发词' : 'Trigger' },
];
filterCats.forEach(cat => {
const pill = text(leftCategoryPills, 'button', cat.label, `anomalous-workbench-pill${sourceFilterCategory === cat.id ? ' is-active' : ''}`);
@@ -394,13 +397,16 @@ function buildPromptComposer(owner, container, options = {}) {
const rightActions = text(rightHeader, 'div', '', 'anomalous-mixer-actions');
// Right quick extract: suck into right mixer directly
const rightSuckNodeBtn = text(rightActions, 'button', `🎯 ${t('promptReadSelectedNode')}`, 'anomalous-btn-ghost anomalous-btn-sm');
const rightSuckNodeBtn = text(rightActions, 'button', '', 'anomalous-btn-ghost anomalous-btn-sm');
rightSuckNodeBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>${t('promptReadSelectedNode')}`;
rightSuckNodeBtn.title = window.anomalous_browser_lang === 'zh' ? '直接将画布选中节点的提示词作为积木吸入当前拼装台' : 'Extract node prompt directly into current mixer track';
const smartSortBtn = text(rightActions, 'button', `🪄 ${t('promptSortByCategory')}`, 'anomalous-mixer-smart-sort-btn');
const smartSortBtn = text(rightActions, 'button', '', 'anomalous-mixer-smart-sort-btn');
smartSortBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>${t('promptSortByCategory')}`;
smartSortBtn.title = window.anomalous_browser_lang === 'zh' ? '按 [通用底模 ➔ 风格氛围 ➔ 主体内容 ➔ LoRA/触发词] 自动排序' : 'Auto sort: [Base ➔ Style ➔ Subject ➔ Trigger]';
const clearRightBtn = text(rightActions, 'button', '🧹', 'anomalous-btn-ghost');
const clearRightBtn = text(rightActions, 'button', '', 'anomalous-btn-ghost');
clearRightBtn.innerHTML = '<svg style="width:13px;height:13px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
clearRightBtn.title = window.anomalous_browser_lang === 'zh' ? '清空当前拼装池' : 'Clear current track';
// Blocks Container & Dropzone
@@ -541,13 +547,13 @@ function buildPromptComposer(owner, container, options = {}) {
if (extractedCount > 0) {
renderSourceCardsList();
showWorkbenchToast(`🎯 ${window.anomalous_browser_lang === 'zh'
showWorkbenchToast(window.anomalous_browser_lang === 'zh'
? `已成功从节点【${heading}】提取 ${extractedCount} 段提示词${intoRightMixer ? '并加入组合' : '并加入左侧词库'}!`
: `Successfully extracted ${extractedCount} prompts from [${heading}]!`}`);
: `Successfully extracted ${extractedCount} prompts from [${heading}]!`);
} else {
anomalousAlert(window.anomalous_browser_lang === 'zh'
? `⚠️ 选中的节点【${heading}】文本内容为空!`
: `⚠️ The text fields in node [${heading}] are empty!`);
? `选中的节点【${heading}】文本内容为空!`
: `The text fields in node [${heading}] are empty!`);
}
}
@@ -557,12 +563,14 @@ function buildPromptComposer(owner, container, options = {}) {
let materialSyncController = null;
async function syncMaterialsIntoSourceDeck() {
materialSyncController?.abort();
if (materialSyncController) {
materialSyncController.abort();
}
const controller = new AbortController();
materialSyncController = controller;
importMaterialsBtn.disabled = true;
importMaterialsBtn.textContent = `⏳ ${window.anomalous_browser_lang === 'zh' ? '同步中...' : 'Syncing...'}`;
let addedCount = 0;
importMaterialsBtn.textContent = window.anomalous_browser_lang === 'zh' ? '正在同步素材...' : 'Syncing...';
try {
const res = await fetch('/anomalous/materials?category=prompts&limit=150', { signal: controller.signal });
@@ -583,7 +591,6 @@ function buildPromptComposer(owner, container, options = {}) {
persisted: true,
filename: item.filename,
});
addedCount++;
}
if (prompts.negative && !sourceCards.some(c => c.content === prompts.negative.trim())) {
sourceCards.push({
@@ -595,7 +602,6 @@ function buildPromptComposer(owner, container, options = {}) {
persisted: true,
filename: item.filename,
});
addedCount++;
}
} catch (err) {}
}
@@ -603,9 +609,9 @@ function buildPromptComposer(owner, container, options = {}) {
if (controller.signal.aborted || !view.isConnected) return;
renderSourceCardsList();
showWorkbenchToast(`📥 ${window.anomalous_browser_lang === 'zh'
showWorkbenchToast(window.anomalous_browser_lang === 'zh'
? (addedCount > 0 ? `已从素材库成功同步并导入 ${addedCount} 条提示词卡片!` : '素材库提示词已是最新状态,未发现新词条。')
: `Synced from Material Library: ${addedCount} new prompts added!`}`);
: `Synced from Material Library: ${addedCount} new prompts added!`);
} catch (error) {
if (error.name !== 'AbortError') {
await anomalousAlert(t('materialLoadError'));
@@ -614,7 +620,7 @@ function buildPromptComposer(owner, container, options = {}) {
if (materialSyncController === controller) {
materialSyncController = null;
importMaterialsBtn.disabled = false;
importMaterialsBtn.textContent = `📥 ${window.anomalous_browser_lang === 'zh' ? '从素材库导入' : 'Import from Library'}`;
importMaterialsBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>${window.anomalous_browser_lang === 'zh' ? '从素材库导入' : 'Import from Library'}`;
}
}
}
@@ -893,7 +899,8 @@ function buildPromptComposer(owner, container, options = {}) {
};
if (card.persisted === false || (card.id.startsWith('node_') && !card.persisted)) {
const saveCardBtn = text(actions, 'button', `💾 ${t('promptSaveCard')}`, 'anomalous-source-action-btn is-save-lib');
const saveCardBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-save-lib');
saveCardBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('promptSaveCard')}`;
saveCardBtn.title = window.anomalous_browser_lang === 'zh' ? '将此卡片存入素材库' : 'Save this card to Material Library';
saveCardBtn.onclick = async (e) => {
e.stopPropagation();
@@ -901,7 +908,8 @@ function buildPromptComposer(owner, container, options = {}) {
};
}
const dockBtn = text(actions, 'button', `➕ ${t('promptAddToPlan')}`, 'anomalous-source-action-btn is-dock');
const dockBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-dock');
dockBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>${t('promptAddToPlan')}`;
dockBtn.title = window.anomalous_browser_lang === 'zh' ? '加入当前拼装组合' : 'Add into mixer track';
dockBtn.onclick = (e) => {
e.stopPropagation();
@@ -919,8 +927,8 @@ function buildPromptComposer(owner, container, options = {}) {
const negCount = draft.plan.parts.filter(p => p.role === 'negative').length;
const tabs = [
{ key: 'positive', label: `✨ ${window.anomalous_browser_lang === 'zh' ? '正向拼装台' : 'Positive'} (${posCount})` },
{ key: 'negative', label: `🚫 ${window.anomalous_browser_lang === 'zh' ? '负向拼装台' : 'Negative'} (${negCount})` },
{ key: 'positive', label: `${window.anomalous_browser_lang === 'zh' ? '正向拼装台' : 'Positive'} (${posCount})` },
{ key: 'negative', label: `${window.anomalous_browser_lang === 'zh' ? '负向拼装台' : 'Negative'} (${negCount})` },
];
for (const tab of tabs) {
@@ -968,7 +976,13 @@ function buildPromptComposer(owner, container, options = {}) {
if (!currentRoleParts.length) {
const dropzoneNotice = text(blocksContainer, 'div', '', 'anomalous-assembly-dropzone');
dropzoneNotice.innerHTML = `
<div style="font-size: 30px; margin-bottom: 8px;">📥</div>
<div style="margin-bottom: 12px; opacity: 0.7;">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</div>
<div style="font-size: 0.92rem; font-weight: 600; color: #e2e8f0;">${window.anomalous_browser_lang === 'zh' ? '将左侧的成型提示词拖放到这里摆放与拼装' : 'Drop prompt cards from the left panel here'}</div>
<div style="font-size: 0.76rem; color: #64748b; margin-top: 4px;">${window.anomalous_browser_lang === 'zh' ? '支持自由调换次序,点击【智能理顺】自动按画质底模➔风格➔主体➔LoRA排序' : 'Drag to reorder anytime, or click Smart Sort to auto-align.'}</div>
`;
@@ -1257,12 +1271,12 @@ function buildPromptComposer(owner, container, options = {}) {
const targets = promptWidgetTargets(node);
if (!node || !targets.length) {
text(targetBar, 'span', `💡 ${t('promptSelectTextNode')}`, 'anomalous-prompt-target-info');
text(targetBar, 'span', t('promptSelectTextNode'), 'anomalous-prompt-target-info');
return;
}
const info = text(targetBar, 'div', '', 'anomalous-prompt-target-info');
info.textContent = `🎯 ${t('materialApplyingTo', { name: materialNodeHeading(node), id: node.id })}`;
info.textContent = t('materialApplyingTo', { name: materialNodeHeading(node), id: node.id });
const actions = text(targetBar, 'div', '', 'anomalous-prompt-target-actions');
const widgetSelect = text(actions, 'select', '');
+42 -24
View File
@@ -1082,11 +1082,15 @@ function renderOverview(content, owner, recipe, references, finish) {
overviewActions.style.alignItems = 'center';
overviewActions.style.gap = '8px';
const heroAppendIcon = recipe?.workflow_scope === 'partial'
? `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>`
: `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
const heroAppend = button(
overviewActions,
`${recipe?.workflow_scope === 'partial' ? '🧩' : '🚀'} ${recipeCanvasActionLabel(recipe)}`,
'',
'anomalous-recipe-btn-primary-action',
);
heroAppend.innerHTML = `${heroAppendIcon}${recipeCanvasActionLabel(recipe)}`;
heroAppend.style.padding = '8px 16px';
heroAppend.style.fontSize = '0.88rem';
heroAppend.onclick = () => {
@@ -1100,11 +1104,13 @@ function renderOverview(content, owner, recipe, references, finish) {
appendText(more, 'summary', t('notebookMore'));
overviewActions.appendChild(more);
const heroEdit = button(more, '✏️ ' + t('recipeEdit'), 'anomalous-btn-ghost');
const heroEdit = button(more, '', 'anomalous-btn-ghost');
heroEdit.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>${t('recipeEdit')}`;
heroEdit.title = t('recipeEdit');
heroEdit.onclick = () => finish('edit');
const heroExport = button(more, '📥 ' + t('recipeExport'), 'anomalous-btn-ghost');
const heroExport = button(more, '', 'anomalous-btn-ghost');
heroExport.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>${t('recipeExport')}`;
heroExport.title = t('recipeExport');
heroExport.onclick = async () => {
try {
@@ -1164,7 +1170,7 @@ function renderOverview(content, owner, recipe, references, finish) {
const advanced = document.createElement('details');
advanced.className = 'anomalous-recipe-advanced-info';
advanced.style.marginBottom = '14px';
appendText(advanced, 'summary', `⚙️ ${t('recipeAdvancedInfo')}`);
appendText(advanced, 'summary', t('recipeAdvancedInfo'));
const fingerprint = document.createElement('div');
fingerprint.className = 'anomalous-recipe-advanced-row';
appendText(fingerprint, 'span', `${t('recipeDetailFingerprint')}:`);
@@ -2031,7 +2037,7 @@ function renderRawNodesLazy(parent, source, options = {}) {
const summary = document.createElement('summary');
summary.className = 'anomalous-recipe-raw-nodes-summary';
summary.textContent = `⚙️ ${t('recipeRawNodesToggle')} (${ordered.length} ${t('recipeRawNodesCount')}) ▾`;
summary.textContent = `${t('recipeRawNodesToggle')} (${ordered.length} ${t('recipeRawNodesCount')}) ▾`;
details.appendChild(summary);
let selecting = false;
@@ -2436,14 +2442,16 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
} finally {
readCurrent.disabled = false;
readCurrent.classList.remove('is-busy');
readCurrent.textContent = originalLabel;
readCurrent.innerHTML = originalLabel;
}
};
const readCurrent = button(sidebarActions, '💾 ' + t('recipeParameterReadCurrent'), 'anomalous-preset-btn-primary');
const readCurrent = button(sidebarActions, '', 'anomalous-preset-btn-primary');
readCurrent.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('recipeParameterReadCurrent')}`;
readCurrent.onclick = readCurrentHandler;
const newSnapshot = button(sidebarActions, '➕ ' + t('recipeParameterNew'), 'anomalous-preset-btn-secondary');
const newSnapshot = button(sidebarActions, '', 'anomalous-preset-btn-secondary');
newSnapshot.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>${t('recipeParameterNew')}`;
newSnapshot.onclick = () => {
const draft = cloneJson({
workflow: baseSource.workflow,
@@ -2532,7 +2540,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
);
};
const rename = button(actions, '✏️', 'anomalous-preset-item-btn');
const rename = button(actions, '', 'anomalous-preset-item-btn');
rename.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
rename.title = t('recipeParameterRename');
rename.onclick = async (e) => {
e.stopPropagation();
@@ -2559,7 +2568,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
}
};
const remove = button(actions, '🗑️', 'anomalous-preset-item-btn is-delete');
const remove = button(actions, '', 'anomalous-preset-item-btn is-delete');
remove.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
remove.title = t('recipeParameterDelete');
remove.onclick = async (e) => {
e.stopPropagation();
@@ -2625,7 +2635,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
const consoleActions = document.createElement('div');
consoleActions.className = 'anomalous-preset-console-actions';
const applyButton = button(consoleActions, '🚀 ' + t('recipeParameterApply'), 'anomalous-preset-btn-primary');
const applyButton = button(consoleActions, '', 'anomalous-preset-btn-primary');
applyButton.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>${t('recipeParameterApply')}`;
applyButton.style.padding = '8px 16px';
applyButton.style.fontSize = '0.88rem';
const applyStatus = appendText(consoleActions, 'small', '', 'anomalous-recipe-header-status');
@@ -2665,7 +2676,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
);
if (parameterState?.selectedFilename) {
const renameHeadingBtn = button(consoleActions, '✏️ ' + t('recipeParameterRename'), 'anomalous-btn-ghost');
const renameHeadingBtn = button(consoleActions, '', 'anomalous-btn-ghost');
renameHeadingBtn.innerHTML = `<svg style="width:13px;height:13px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>${t('recipeParameterRename')}`;
renameHeadingBtn.onclick = async () => {
const newName = await anomalousPrompt(t('recipeParameterRenamePrompt'), currentName);
if (newName === null || !newName.trim() || newName.trim() === currentName) return;
@@ -2698,7 +2710,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
const thumb = document.createElement('img');
thumb.src = outputImageUrl(gallery.images[0]);
compactGallery.appendChild(thumb);
appendText(compactGallery, 'span', `🖼️ ${gallery.images.length}`, 'anomalous-preset-compact-gallery-badge');
const galleryBadge = appendText(compactGallery, 'span', '', 'anomalous-preset-compact-gallery-badge');
galleryBadge.innerHTML = `<svg style="width:11px;height:11px;margin-right:3px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>${gallery.images.length}`;
compactGallery.onclick = () => {
const dialog = document.createElement('dialog');
dialog.className = 'anomalous-recipe-gallery-dialog';
@@ -2724,7 +2737,8 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
card.appendChild(image);
const actions = document.createElement('div');
actions.className = 'anomalous-recipe-gallery-card-actions';
const details = button(actions, `🔎 ${t('materialViewDetails')}`, 'anomalous-btn-primary');
const details = button(actions, '', 'anomalous-btn-primary');
details.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>${t('materialViewDetails')}`;
details.onclick = event => {
event.stopPropagation();
const images = gallery.images;
@@ -2761,13 +2775,13 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
bentoGrid.className = 'anomalous-recipe-bento-grid';
const bentoItems = [
{ label: t('recipeDetailSteps') || '步数', val: params.steps, icon: '⚡' },
{ label: t('recipeDetailCFG') || 'CFG Scale', val: params.cfg, icon: '🎯' },
{ label: t('recipeDetailSampler') || '采样器', val: params.sampler_name || params.samplers, icon: '🎲' },
{ label: t('recipeDetailScheduler') || '调度器', val: params.scheduler, icon: '📈' },
{ label: t('recipeDetailResolution') || '分辨率', val: resDisplay || params.resolution, icon: '📐' },
{ label: t('recipeDetailDenoise') || '降噪比', val: params.denoise, icon: '🌊' },
{ label: t('recipeDetailSeed') || '随机种子', val: params.seed, icon: '🌱', isSeed: true },
{ label: t('recipeDetailSteps') || '步数', val: params.steps, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' },
{ label: t('recipeDetailCFG') || 'CFG Scale', val: params.cfg, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></svg>' },
{ label: t('recipeDetailSampler') || '采样器', val: params.sampler_name || params.samplers, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="8" cy="8" r="1.5"/><circle cx="16" cy="16" r="1.5"/><circle cx="12" cy="12" r="1.5"/></svg>' },
{ label: t('recipeDetailScheduler') || '调度器', val: params.scheduler, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>' },
{ label: t('recipeDetailResolution') || '分辨率', val: resDisplay || params.resolution, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>' },
{ label: t('recipeDetailDenoise') || '降噪比', val: params.denoise, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2.69l5.66 5.66a8 8 0 11-11.31 0z"/></svg>' },
{ label: t('recipeDetailSeed') || '随机种子', val: params.seed, icon: '<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>', isSeed: true },
].filter(item => item.val !== undefined && item.val !== null && item.val !== '');
if (bentoItems.length > 0) {
@@ -2781,7 +2795,10 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
labelRow.style.justifyContent = 'space-between';
labelRow.style.alignItems = 'center';
appendText(labelRow, 'span', `${item.icon} ${item.label}`, 'anomalous-recipe-bento-label');
const bentoLabel = document.createElement('span');
bentoLabel.className = 'anomalous-recipe-bento-label';
bentoLabel.innerHTML = `${item.icon}${item.label}`;
labelRow.appendChild(bentoLabel);
if (item.isSeed) {
const copySeedBtn = document.createElement('button');
@@ -2818,14 +2835,15 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
if (baseModelVal) {
const modelCard = document.createElement('div');
modelCard.className = 'anomalous-recipe-model-highlight-card';
modelCard.innerHTML = `<div style="display:flex;align-items:center;gap:8px;"><span style="font-size:1.1rem;">🧠</span><span style="font-size:0.75rem;color:#94a3b8;text-transform:uppercase;font-weight:600;">${t('recipeDetailBaseModel') || '底模'}</span></div><div style="font-size:0.9rem;font-weight:600;color:#f8fafc;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(String(baseModelVal))}">${escapeHtml(String(baseModelVal))}</div>`;
modelCard.innerHTML = `<div style="display:flex;align-items:center;gap:8px;"><span style="display:inline-flex;align-items:center;color:#dc143c;"><svg style="width:16px;height:16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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></span><span style="font-size:0.75rem;color:#94a3b8;text-transform:uppercase;font-weight:600;">${t('recipeDetailBaseModel') || '底模'}</span></div><div style="font-size:0.9rem;font-weight:600;color:#f8fafc;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escapeHtml(String(baseModelVal))}">${escapeHtml(String(baseModelVal))}</div>`;
modelsDeck.appendChild(modelCard);
}
if (Array.isArray(params.loras) && params.loras.length > 0) {
const loraSection = document.createElement('div');
loraSection.className = 'anomalous-recipe-lora-stack';
appendText(loraSection, 'div', `⚡ ${t('recipeDetailLoraSummary') || 'LoRA 阵容'} (${params.loras.length})`, 'anomalous-recipe-bento-label');
const loraHeader = appendText(loraSection, 'div', '', 'anomalous-recipe-bento-label');
loraHeader.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>${t('recipeDetailLoraSummary') || 'LoRA 阵容'} (${params.loras.length})`;
const loraGrid = document.createElement('div');
loraGrid.style.display = 'grid';
+36 -22
View File
@@ -851,13 +851,15 @@ function buildRecipeStudioTopbar(owner) {
viewSwitch.append(gridBtn, listBtn);
right.appendChild(viewSwitch);
const saveBtn = appendText(right, 'button', `💾 ${t('recipeSaveCurrent')}`, 'anomalous-recipe-topbar-btn is-primary');
const saveBtn = appendText(right, 'button', '', 'anomalous-recipe-topbar-btn is-primary');
saveBtn.dataset.recipeSaveCurrent = 'true';
saveBtn.type = 'button';
saveBtn.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('recipeSaveCurrent')}`;
saveBtn.onclick = () => owner.handleSaveRecipe();
const importBtn = appendText(right, 'button', `📥 ${t('recipeImport')}`, 'anomalous-recipe-topbar-btn');
const importBtn = appendText(right, 'button', '', 'anomalous-recipe-topbar-btn');
importBtn.type = 'button';
importBtn.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>${t('recipeImport')}`;
importBtn.onclick = () => {
const input = document.createElement('input');
input.type = 'file';
@@ -974,19 +976,22 @@ function createRecipeQuickSpecs(params) {
const loras = Array.isArray(params.loras) ? params.loras : [];
if (loras.length > 0) {
const loraTag = appendText(strip, 'span', `🧩 ${loras.length} ${t('recipeCardSpecsLoras')}`, 'anomalous-recipe-spec-tag is-lora');
const loraTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag is-lora');
loraTag.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>${loras.length} ${t('recipeCardSpecsLoras')}`;
loraTag.title = loras.map((l) => (typeof l === 'object' && l?.name ? l.name : String(l))).join(', ');
}
if (params.steps) {
const samplerName = params.sampler_name ? ` · ${params.sampler_name}` : '';
const stepTag = appendText(strip, 'span', `⏱️ ${params.steps} ${t('recipeCardSpecsSteps')}${samplerName}`, 'anomalous-recipe-spec-tag is-step');
const stepTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag is-step');
stepTag.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>${params.steps} ${t('recipeCardSpecsSteps')}${samplerName}`;
stepTag.title = `${params.steps} ${t('recipeCardSpecsSteps')}${samplerName}`;
}
const formattedRes = formatRecipeResolution(params.resolution);
if (formattedRes) {
const resTag = appendText(strip, 'span', `📐 ${formattedRes}`, 'anomalous-recipe-spec-tag');
const resTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag');
resTag.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>${formattedRes}`;
resTag.title = `${t('recipeCardSpecsResolution')}: ${formattedRes}`;
}
@@ -1044,19 +1049,23 @@ function createRecipeCard(owner, recipe) {
const baseModelStr = data.params?.baseModel || data.params?.baseModels;
if (baseModelStr) {
const pill = appendText(bottomChips, 'span', `📦 ${modelDisplayName(baseModelStr)}`, 'anomalous-recipe-cover-pill');
const pill = document.createElement('span');
pill.className = 'anomalous-recipe-cover-pill';
pill.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:4px;"><path d="m21 16-9 5-9-5V8l9-5 9 5v8z"/><path d="m3.27 6.96 8.73 4.84 8.73-4.84"/><path d="M12 22.08V11.8"/></svg><span>${escapeHtml(modelDisplayName(baseModelStr))}</span>`;
pill.title = String(baseModelStr);
bottomChips.appendChild(pill);
}
mediaWrap.appendChild(bottomChips);
// Scope Pill (Top-right of cover)
const isPartial = data.workflow_scope === 'partial';
appendText(
mediaWrap,
'span',
isPartial ? '🧩 ' + t('recipeScopePill_partial') : '⚡ ' + t('recipeScopePill_complete'),
`anomalous-recipe-scope-pill ${isPartial ? 'is-partial' : 'is-complete'}`,
);
const scopePill = document.createElement('span');
scopePill.className = `anomalous-recipe-scope-pill ${isPartial ? 'is-partial' : 'is-complete'}`;
const scopeSvg = isPartial
? `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:3px;"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v6"/><path d="M9 6h6"/></svg>`
: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:3px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`;
scopePill.innerHTML = `${scopeSvg}<span>${t(isPartial ? 'recipeScopePill_partial' : 'recipeScopePill_complete')}</span>`;
mediaWrap.appendChild(scopePill);
card.appendChild(mediaWrap);
// 2. Card Body
@@ -1093,17 +1102,15 @@ function createRecipeCard(owner, recipe) {
body.appendChild(tags);
}
// Footer: Primary Action Button ("🚀 载入画布" / "🧩 追加画布") + Mini Actions
// Footer: Primary Action Button + Mini Actions
const footer = document.createElement('div');
footer.className = 'anomalous-recipe-card-footer';
const appendBtn = appendText(
footer,
'button',
`${isPartial ? '🧩' : '🚀'} ${t(isPartial ? 'recipeAppendCanvas' : 'recipeOpenCanvas')}`,
'anomalous-recipe-btn-primary-action anomalous-tooltip-target',
);
const appendBtn = document.createElement('button');
appendBtn.type = 'button';
appendBtn.className = 'anomalous-recipe-btn-primary-action anomalous-tooltip-target';
const actionSvg = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display:inline-block;vertical-align:-1px;margin-right:4px;"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
appendBtn.innerHTML = `${actionSvg}<span>${t(isPartial ? 'recipeAppendCanvas' : 'recipeOpenCanvas')}</span>`;
appendBtn.removeAttribute('title');
appendBtn.setAttribute('data-tooltip', isPartial ? t('recipeAppendCanvas') : t('recipeOpenCanvas'));
appendBtn.setAttribute('data-tooltip-pos', 'top');
@@ -1115,12 +1122,15 @@ function createRecipeCard(owner, recipe) {
await applyRecipeToCanvas(owner, fullRecipe);
}, isPartial ? 'recipeAppendError' : 'recipeOpenError');
};
footer.appendChild(appendBtn);
const miniActions = document.createElement('div');
miniActions.className = 'anomalous-recipe-card-mini-actions';
const editBtn = appendText(miniActions, 'button', '✏️', 'anomalous-recipe-card-mini-btn anomalous-tooltip-target');
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target';
editBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>`;
editBtn.removeAttribute('title');
editBtn.setAttribute('data-tooltip', t('recipeEdit'));
editBtn.setAttribute('data-tooltip-pos', 'top');
@@ -1129,8 +1139,10 @@ function createRecipeCard(owner, recipe) {
runRecipeCardAction(editBtn, () => editRecipe(owner, recipe?.data || {}, recipe.filename), 'recipeUpdateError');
};
const exportBtn = appendText(miniActions, 'button', '📥', 'anomalous-recipe-card-mini-btn anomalous-tooltip-target');
const exportBtn = document.createElement('button');
exportBtn.type = 'button';
exportBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target';
exportBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
exportBtn.removeAttribute('title');
exportBtn.setAttribute('data-tooltip', t('recipeExport'));
exportBtn.setAttribute('data-tooltip-pos', 'top');
@@ -1139,8 +1151,10 @@ function createRecipeCard(owner, recipe) {
runRecipeCardAction(exportBtn, () => exportRecipePackage(recipe.filename), 'recipeExportError');
};
const removeBtn = appendText(miniActions, 'button', '🗑️', 'anomalous-recipe-card-mini-btn is-delete anomalous-tooltip-target');
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'anomalous-recipe-card-mini-btn is-delete anomalous-tooltip-target';
removeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`;
removeBtn.removeAttribute('title');
removeBtn.setAttribute('data-tooltip', t('recipeDelete'));
removeBtn.setAttribute('data-tooltip-pos', 'top');
+38 -29
View File
@@ -13,13 +13,13 @@ const t = (key, params) => translate(key, params);
const SCAN_RADAR_ICON_SVG = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="12" r="9"/><path d="M12 12 18.5 5.5"/><circle cx="12" cy="12" r="2"/><path d="M12 7a5 5 0 0 1 5 5"/></svg>`;
const SIDEBAR_ICONS = {
MODELS: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
MODELS: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><path d="m21 16-9 5-9-5V8l9-5 9 5v8z"/><path d="m3.27 6.96 8.73 4.84 8.73-4.84"/><path d="M12 22.08V11.8"/></svg>`,
GALLERY: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
RECIPES: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`,
RECIPES: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v6"/><path d="M9 6h6"/><path d="M7.8 7.8l8.4 8.4"/></svg>`,
DOCK: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>`,
HELP: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
DOCTOR: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>`,
ASSISTANT: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/><line x1="8" y1="19" x2="16" y2="19"/></svg>`,
ASSISTANT: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="m15 4 5 5"/><path d="m18 7-8.5 8.5a2.12 2.12 0 1 1-3-3L15 4Z"/><path d="m2 22 5-5"/><path d="M19 13v2"/><path d="M21 15h-2"/><path d="M7 2v2"/><path d="M9 4H7"/></svg>`,
SETTINGS: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
FOLDER: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-3px;margin-right:7px;"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>`,
CHEVRON_UP: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-2px;margin-right:4px;"><polyline points="18 15 12 9 6 15"/></svg>`,
@@ -218,14 +218,14 @@ export function createDOM() {
}
}, 1000);
const spacer = document.createElement('div');
spacer.style.flex = '1 1 auto';
const leftGroup = document.createElement('div');
leftGroup.className = 'anomalous-header-group';
leftGroup.className = 'anomalous-header-group anomalous-header-left';
const centerGroup = document.createElement('div');
centerGroup.className = 'anomalous-header-group anomalous-header-center';
const rightGroup = document.createElement('div');
rightGroup.className = 'anomalous-header-group';
rightGroup.className = 'anomalous-header-group anomalous-header-right';
// We will define hideAllPanels as a class method instead of a local closure to make it globally accessible.
@@ -341,9 +341,11 @@ export function createDOM() {
this.showRecipes();
};
rightGroup.appendChild(modelsBtn);
rightGroup.appendChild(galleryBtn);
rightGroup.appendChild(nbBtn);
centerGroup.appendChild(modelsBtn);
centerGroup.appendChild(galleryBtn);
centerGroup.appendChild(nbBtn);
leftGroup.appendChild(dockBtn);
leftGroup.appendChild(helpBtn);
const apiKeyBtn = document.createElement('button');
apiKeyBtn.id = 'anomalous-api-btn';
@@ -367,7 +369,7 @@ export function createDOM() {
scanBtn.removeAttribute('title');
scanBtn.setAttribute('aria-label', t('sidebarScanWizard'));
scanBtn.setAttribute('data-tooltip', t('sidebarScanWizard'));
scanBtn.setAttribute('data-tooltip-pos', 'right');
scanBtn.setAttribute('data-tooltip-pos', 'top');
setScanButtonState(scanBtn, false);
scanBtn.style.background = 'transparent';
scanBtn.style.border = 'none';
@@ -728,7 +730,11 @@ export function createDOM() {
left.style.gap = '16px';
const iconEl = document.createElement('div');
iconEl.innerText = icon;
if (typeof icon === 'string' && icon.startsWith('<')) {
iconEl.innerHTML = icon;
} else {
iconEl.innerText = icon;
}
iconEl.style.fontSize = '1.4em';
iconEl.style.lineHeight = '1.2';
iconEl.style.width = '24px';
@@ -819,7 +825,7 @@ export function createDOM() {
pRow.style.display = 'flex';
pRow.style.alignItems = 'center';
pRow.style.gap = '8px';
pRow.innerHTML = `<span style="font-size:0.9em; color:#ddd;">💾 ${t('sidebarPhysicalRename')}</span>`;
pRow.innerHTML = `<span style="font-size:0.9em; color:#ddd; display:inline-flex; align-items:center; gap:6px;"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('sidebarPhysicalRename')}</span>`;
pRow.appendChild(physicalSwitch);
const pDesc = document.createElement('div');
@@ -846,7 +852,8 @@ export function createDOM() {
dualChannelRow.appendChild(vContainer);
dualChannelRow.appendChild(pContainer);
s2List.appendChild(createListRow('📝', 'sidebarNormalize', 'sidebarNormalizeDesc', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); })));
const editSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
s2List.appendChild(createListRow(editSvg, 'sidebarNormalize', 'sidebarNormalizeDesc', createMaterialSwitch(enableRename, (s) => { enableRename = s; updateDualChannelUI(); })));
s2List.lastChild.style.borderBottom = 'none';
s2List.appendChild(dualChannelRow);
updateDualChannelUI();
@@ -865,9 +872,12 @@ export function createDOM() {
const s3List = document.createElement('div');
const isInject = localStorage.getItem('anomalous_inject_hash') !== 'false';
s3List.appendChild(createListRow('📦', 'sidebarProvenance', 'sidebarProvenanceDesc', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
s3List.appendChild(createListRow('⚠️', 'sidebarOverwrite', 'sidebarOverwriteDesc', createMaterialSwitch(enableForceOverwrite, (s) => enableForceOverwrite = s)));
s3List.appendChild(createListRow('🪄', 'sidebarSmartFix', 'sidebarSmartFixDesc', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s)));
const cubeSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#dc143c;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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 warnSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#f59e0b;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
const wandSvg = '<svg style="width:20px;height:20px;vertical-align:middle;stroke:#8b5cf6;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>';
s3List.appendChild(createListRow(cubeSvg, 'sidebarProvenance', 'sidebarProvenanceDesc', createMaterialSwitch(isInject, (s) => localStorage.setItem('anomalous_inject_hash', s ? 'true' : 'false'))));
s3List.appendChild(createListRow(warnSvg, 'sidebarOverwrite', 'sidebarOverwriteDesc', createMaterialSwitch(enableForceOverwrite, (s) => enableForceOverwrite = s)));
s3List.appendChild(createListRow(wandSvg, 'sidebarSmartFix', 'sidebarSmartFixDesc', createMaterialSwitch(enableAutoCheck, (s) => enableAutoCheck = s)));
s3List.lastChild.style.borderBottom = 'none';
section3.appendChild(s3List);
@@ -1125,11 +1135,10 @@ export function createDOM() {
rightGroup.appendChild(dockBtn);
rightGroup.appendChild(closeBtn);
header.appendChild(leftGroup);
header.appendChild(spacer);
header.appendChild(centerGroup);
header.appendChild(rightGroup);
const settingsHubModal = document.createElement('div');
@@ -1159,7 +1168,7 @@ export function createDOM() {
scanBtn.removeAttribute('title');
scanBtn.setAttribute('aria-label', t('sidebarScanWizard'));
scanBtn.setAttribute('data-tooltip', t('sidebarScanWizard'));
scanBtn.setAttribute('data-tooltip-pos', 'right');
scanBtn.setAttribute('data-tooltip-pos', 'top');
setScanButtonState(scanBtn, isCurrentlyScanning);
helpBtn.innerHTML = `${SIDEBAR_ICONS.HELP}<span class="anomalous-btn-text">${t('help')}</span>`;
nbBtn.removeAttribute('title');
@@ -1168,13 +1177,13 @@ export function createDOM() {
nbBtn.innerHTML = `${SIDEBAR_ICONS.RECIPES}<span class="anomalous-btn-text">${t('recipeTitle')}</span>`;
const dBtn = document.getElementById('anomalous-doctor-btn');
if (dBtn) { dBtn.removeAttribute('title'); dBtn.setAttribute('data-tooltip', t('sidebarDoctor')); dBtn.setAttribute('data-tooltip-pos', 'right'); }
if (dBtn) { dBtn.removeAttribute('title'); dBtn.setAttribute('data-tooltip', t('sidebarDoctor')); dBtn.setAttribute('data-tooltip-pos', 'top'); }
const aBtn = document.getElementById('anomalous-assistant-btn');
if (aBtn) { aBtn.removeAttribute('title'); aBtn.setAttribute('data-tooltip', t('sidebarAssistant')); aBtn.setAttribute('data-tooltip-pos', 'right'); }
if (aBtn) { aBtn.removeAttribute('title'); aBtn.setAttribute('data-tooltip', t('sidebarAssistant')); aBtn.setAttribute('data-tooltip-pos', 'top'); }
const iBtn = document.getElementById('anomalous-materials-btn');
if (iBtn) { iBtn.removeAttribute('title'); iBtn.setAttribute('aria-label', t('materialLibrary')); iBtn.setAttribute('data-tooltip', t('materialLibrary')); iBtn.setAttribute('data-tooltip-pos', 'right'); }
if (iBtn) { iBtn.removeAttribute('title'); iBtn.setAttribute('aria-label', t('materialLibrary')); iBtn.setAttribute('data-tooltip', t('materialLibrary')); iBtn.setAttribute('data-tooltip-pos', 'top'); }
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'); }
if (sBtn) { sBtn.removeAttribute('title'); sBtn.setAttribute('data-tooltip', t('sidebarSettings')); sBtn.setAttribute('data-tooltip-pos', 'top'); }
// Reset dynamic panels so they re-render in new language
if (window.anomalousBrowserInstance) {
@@ -1647,7 +1656,7 @@ export function createDOM() {
settingsBtn.removeAttribute('title');
settingsBtn.setAttribute('aria-label', t('sidebarSettings'));
settingsBtn.setAttribute('data-tooltip', t('sidebarSettings'));
settingsBtn.setAttribute('data-tooltip-pos', 'right');
settingsBtn.setAttribute('data-tooltip-pos', 'top');
settingsBtn.style.background = 'transparent';
settingsBtn.style.border = 'none';
settingsBtn.style.borderRadius = '6px';
@@ -1679,7 +1688,7 @@ export function createDOM() {
importBtn.removeAttribute('title');
importBtn.setAttribute('aria-label', t('materialLibrary'));
importBtn.setAttribute('data-tooltip', t('materialLibrary'));
importBtn.setAttribute('data-tooltip-pos', 'right');
importBtn.setAttribute('data-tooltip-pos', 'top');
importBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M21 8a2 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"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>`;
importBtn.style.background = 'transparent';
importBtn.style.border = 'none';
@@ -1697,7 +1706,7 @@ export function createDOM() {
doctorBtn.removeAttribute('title');
doctorBtn.setAttribute('aria-label', t('sidebarDoctor'));
doctorBtn.setAttribute('data-tooltip', t('sidebarDoctor'));
doctorBtn.setAttribute('data-tooltip-pos', 'right');
doctorBtn.setAttribute('data-tooltip-pos', 'top');
doctorBtn.innerHTML = SIDEBAR_ICONS.DOCTOR;
doctorBtn.style.background = 'transparent';
doctorBtn.style.border = 'none';
@@ -1733,7 +1742,7 @@ export function createDOM() {
assistantBtn.removeAttribute('title');
assistantBtn.setAttribute('aria-label', t('sidebarAssistant'));
assistantBtn.setAttribute('data-tooltip', t('sidebarAssistant'));
assistantBtn.setAttribute('data-tooltip-pos', 'right');
assistantBtn.setAttribute('data-tooltip-pos', 'top');
assistantBtn.innerHTML = SIDEBAR_ICONS.ASSISTANT;
assistantBtn.style.background = 'transparent';
assistantBtn.style.border = 'none';
+383 -277
View File
File diff suppressed because it is too large Load Diff