Migrate detail doctor and notebook i18n

This commit is contained in:
DemonGatanjieu
2026-08-06 17:11:17 +08:00
parent bc66782d90
commit 09d3eccf40
6 changed files with 509 additions and 192 deletions
+23
View File
@@ -1,5 +1,28 @@
# AI Changelog
## [Snapshot] 2026-08-06 - Migrate detail, doctor, assistant, picker, and notebook i18n
**Implemented**
- Migrated `web/modules/ui_detail.js`, `web/modules/ui_doctor.js`, and `web/modules/ui_notebooks.js` to the shared translator.
- Added locale entries for model editing, batch selection, Doctor diagnostics, Node Assistant actions, model replacement/insertion, history cards, and Notebook validation.
- Replaced language-condition branches with parameterized translation calls while keeping model names, paths, node types, scan filenames, and API messages dynamic.
- Preserved status styling for selected counts, empty/error states, healthy Doctor output, compatible-model messages, and missing-cover messages through DOM composition.
- Added the missing `galleryCancel` key used by the previous Gallery migration snapshot.
**Validation**
- JavaScript syntax checks passed for every file under `web/`.
- All 189 static translator calls in the migrated UI modules resolve in both `zh` and `en` dictionaries.
- Locale interpolation checks passed for Doctor scan status, picker selection, Assistant history counts, and Detail selection counts.
- `node tests/recipe_parser_roundtrip.mjs` passed.
- Python compilation passed for every `api/*.py` module and `scraper.py` using the bundled Python runtime.
- Targeted `git diff --check` passed; the existing user change in `README.md` was kept outside this snapshot.
**Next**
- Continue with the next Stage 2 group only after review; Sidebar, Recipe, and hash-resolver branches remain untouched.
## [Snapshot] 2026-08-06 - Migrate grid and gallery i18n
**Implemented**
+2
View File
@@ -56,6 +56,8 @@ Every i18n or licensing change is a local, reviewable snapshot. The implementati
The second migration snapshot extends the shared translation contract to `ui_grid.js` and `ui_gallery.js`. Gallery confirmation and cover-selection banners are assembled with DOM nodes and `textContent` rather than translated raw HTML; user-controlled model names remain text content. These modules resolve strings at render time through `translate`, so existing global language changes continue to be reflected without introducing a new locale selector.
The third migration snapshot extends the same contract to `ui_detail.js`, `ui_doctor.js`, and `ui_notebooks.js`. Detail editing, model selection, Doctor diagnostics, the Node Assistant, the model picker, and Notebook validation now resolve user-facing strings through `translate` with parameter interpolation for counts, paths, model names, and scan status. Dynamic model/path/node data remains outside the locale dictionaries; rich UI state is composed with DOM nodes where translated text replaced previously formatted status HTML.
## 2. Directory Structure (核心文件结构)
```text
+282
View File
@@ -96,6 +96,147 @@ export const i18n = {
gallerySelectingCoverPrefix: '正在为模型 ',
gallerySelectingCoverSuffix: ' 选择封面。请点击下方的图片。',
galleryLoadImagesError: '加载历史图片失败',
galleryCancel: '取消',
detailBackDoctor: '⬅ 返回医生面板',
detailBackAssistant: '⬅ 返回助手面板',
detailJumpToBottom: '跳到底部',
detailEdit: '⚙️ 编辑',
detailNotesTitle: '📓 专属备注 (Notes)',
detailEditNotes: '编辑备注',
detailGeneratedGallery: '🖼️ 查看历史生成作品',
detailTrainedWords: '训练词:',
detailDescription: '描述:',
detailNotes: '备注:',
detailNoCompatibleModels: '没有找到兼容模型。',
detailCompatibleLoadFailed: '加载失败。',
detailToggleSound: '开启/关闭声音',
detailNoCover: '暂无封面',
detailPickGallery: '从历史图库挑选',
detailUploadLocal: '从本地上传',
detailUploadError: '错误: ',
detailUploadFailed: '上传失败: ',
detailModelInfo: '编辑模型信息',
detailFile: '文件:',
detailCustomNamePlaceholder: '自定义名称 (留空则使用原名)',
detailNotesPlaceholder: '📓 专属备注... (支持多行)',
detailPhysicalRename: '同步物理重命名',
detailPhysicalRenameDesc: '若不勾选则仅虚拟重命名,安全不破坏原文件。',
detailResetAll: '重置设置',
detailResetConfirm: '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?',
detailResetCoverPreserved: '未找到可恢复的 C 站备份或原始封面。为避免丢失唯一图片,当前封面已保留;自定义名称和备注仍已重置。',
detailResetCoverFailed: '封面恢复未能完整完成,请检查文件权限或占用情况;自定义名称和备注仍已重置。',
detailResetFailed: '重置失败。',
detailCancel: '取消',
detailSaveChanges: '保存更改',
detailSaving: '保存中...',
detailSelectModels: '选择模型',
detailFolders: '目录列表',
detailSelectedItems: '已选 {count} 项',
detailSelectedItemsPrefix: '已选 ',
detailSelectedItemsSuffix: ' 项',
detailNothingSelected: '暂未选择',
detailLoadedModels: '已加载 {loaded} / {total} 个模型',
detailComputing: '计算中...',
detailSelectNoPreview: '选无预览图',
detailSelectNoDesc: '选无介绍',
detailSelectAll: '全选',
detailClear: '清空',
detailConfirm: '确定',
detailLoading: '加载中 (Loading)...',
detailAllLoaded: '✅ 已加载全部',
detailScrollMore: '滑动加载更多...',
detailNoModels: '没有找到模型。',
detailApiRestart: '❌ 请求后端 API 失败!\n\n你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。',
doctorTitle: '全局体检中心',
doctorAutoScanOn: 'ComfyUI 启动时自动检测并替换缺失模型 [已开启]',
doctorAutoScanOff: 'ComfyUI 启动时自动检测并替换缺失模型 [默认关闭]',
doctorRefresh: '🔄 刷新缓存',
doctorRefreshTitle: '重新读取最新模型列表并清除报错',
doctorGraphUnavailable: '当前画布或节点不可用。',
doctorMissingChainInputs: '节点没有完整的 MODEL/CLIP 输入。',
doctorUnconnectedChainInputs: 'MODEL/CLIP 输入尚未全部连接。',
doctorMissingChainOutputs: '节点没有完整的 MODEL/CLIP 输出。',
doctorAmbiguousBranches: '检测到多个下游分支,请先整理或选择明确链路。',
doctorInvalidDownstream: '现有下游连线无效,无法安全改写。',
doctorUnsupportedInsertion: '当前节点不支持此插入方式。',
doctorCreateLoraFailed: '无法创建标准 LoRA Loader 节点。',
doctorLoraNotReady: 'LoRA 列表尚未就绪,请刷新 ComfyUI 模型列表后重试。',
assistantSelectNode: '请在画布中点击选中任意节点',
assistantNoModelParameter: '该节点没有受支持的模型参数',
assistantSelectedNode: '当前选中节点',
assistantQuickActions: '快捷操作',
assistantChangeWidget: '更换 {name}',
assistantChangeCurrent: '更换当前模型',
assistantVisualPicker: '{type} · 可视化选择',
assistantInsertBefore: '前方插入 LoRA',
assistantInsertAfter: '后方插入 LoRA',
assistantConnectInputs: '接入节点输入端',
assistantConnectOutputs: '接入节点输出端',
doctorTotal: '总模型',
doctorHealthy: '🟢 健康',
doctorMissing: '🔴 缺失',
doctorNoWorkflowModels: '当前工作流没有任何模型节点',
doctorAutoRedirected: '自动重定向',
doctorReady: '正常',
doctorMissingStatus: '丢失',
doctorCivitai: '🌐 C站',
doctorDeepHashScan: '🔍 深度哈希扫描',
doctorScanStarting: '⏳ 正在启动扫描引擎...',
doctorScanning: '⏳ 扫描中 ({current}/{total}) {filename}',
doctorScanError: '❌ 扫描过程中发生错误: ',
doctorMatching: '⏳ 正在匹配并替换飘红节点...',
doctorDeepScanSummary: '共深度扫描了 {total} 个缺失信息的模型。',
doctorNoLocalMatch: '❌ 扫描结束,本地未匹配到模型。\n\n{summary}\n\n这说明模型可能真的不在您的硬盘里,或者您删除了原本记录着哈希的源文件。\n请点击卡片上的【🌐 C站】去云端下载。',
doctorScanSuccess: '✅ 深度扫描成功!已自动修复该节点!\n\n{summary}',
doctorPollError: '❌ 状态轮询出错: ',
doctorScanFailed: '❌ 扫描出错: ',
doctorManualReplace: '🔀 手动替换',
doctorOriginalPath: '原路径: {path}',
assistantPreviewFailed: '无法加载预览',
assistantHistoryTitle: '🖼️ 历史生成图 ({count})',
assistantViewAll: '查看全部 →',
assistantRestoreWorkflow: '点击恢复此工作流',
assistantViewGallery: '点击查看完整图库',
pickerNoCompatible: '没有可供选择的兼容模型。',
pickerEyebrow: '节点助手 · 模型选择器',
pickerInsertBeforeTitle: '⬅ 在节点前方插入 LoRA',
pickerInsertAfterTitle: '在节点后方插入 LoRA ➡',
pickerChangeTitle: '🔀 更换当前模型',
pickerFolders: '📁 文件夹',
pickerSearch: '🔍 搜索名称或完整路径…',
pickerMainAll: '主模型:全部类型',
pickerNameAsc: '名称 A–Z',
pickerNameDesc: '名称 Z–A',
pickerByFolder: '按文件夹',
pickerLoadingCovers: '⏳ 正在加载模型封面…',
pickerCancel: '取消',
pickerInsertConfirm: '插入并自动连线',
pickerReplaceConfirm: '确认替换',
pickerCoversFailed: '⚠️ 封面加载失败,仍可按名称选择模型。',
doctorNodeLabel: '节点',
doctorAllHealthy: '🎉 太棒了!当前工作流中所有模型均在本地就绪。',
doctorLoadingPreview: '加载预览...',
doctorNoPreview: '暂无预览图',
doctorProfile: '📖 查看档案',
doctorBase: '基础模型',
doctorTriggerWords: '触发词',
doctorClickCopy: '点击复制',
doctorCopyAll: '📋 全部复制',
doctorNotes: '📝 我的备注',
pickerSelected: '已选择: {path}',
pickerChooseModel: '请选择一个模型。节点连接和强度不会被改动。',
pickerCompatibleMain: '✨ 已根据主模型 {name} 自动筛选兼容 LoRA;可在上方切换为全部类型。',
pickerCompatibleTag: '✨ 未找到已连接主模型,已根据当前 LoRA 的 {name} 标注筛选。',
pickerCompatibleNoTags: 'ℹ 已识别主模型 {name},但没有同类 LoRA 标注,因此暂时显示全部。',
pickerCompatibleUnknown: 'ℹ 未识别到主模型类型,暂时显示全部 LoRA;你仍可手动按基础模型筛选。',
pickerAllModels: '全部模型',
pickerResults: '{count} 个结果',
pickerCurrent: '当前',
pickerBaseModel: '基础模型',
pickerRoot: '根目录',
pickerNoMatches: '没有符合条件的模型。',
pickerOperationFailed: '操作失败:',
notebookSelectMain: '请先选择一个主模型。',
closeHelp: '关闭说明',
delSure: '⚠️ 确定吗?',
dockTitle: '侧边停靠',
@@ -446,6 +587,147 @@ export const i18n = {
gallerySelectingCoverPrefix: 'Selecting cover for ',
gallerySelectingCoverSuffix: '. Click an image below.',
galleryLoadImagesError: 'Error loading images',
galleryCancel: 'Cancel',
detailBackDoctor: '⬅ Back to Doctor',
detailBackAssistant: '⬅ Back to Assistant',
detailJumpToBottom: 'Jump to bottom',
detailEdit: '⚙️ Edit',
detailNotesTitle: '📓 Custom Notes',
detailEditNotes: 'Edit Notes',
detailGeneratedGallery: '🖼️ View Generated Gallery',
detailTrainedWords: 'Trained Words:',
detailDescription: 'Description:',
detailNotes: 'Notes:',
detailNoCompatibleModels: 'No compatible models found.',
detailCompatibleLoadFailed: 'Failed to load.',
detailToggleSound: 'Toggle Sound',
detailNoCover: 'No Cover',
detailPickGallery: 'Pick from Gallery',
detailUploadLocal: 'Upload Local',
detailUploadError: 'Error: ',
detailUploadFailed: 'Upload failed: ',
detailModelInfo: 'Edit Model Info',
detailFile: 'File:',
detailCustomNamePlaceholder: 'Custom Name (Leave empty to use original)',
detailNotesPlaceholder: '📓 Custom Notes...',
detailPhysicalRename: 'Physical Rename on Disk',
detailPhysicalRenameDesc: 'Virtual rename only if unchecked. Safe for originals.',
detailResetAll: 'Reset All',
detailResetConfirm: 'Reset all custom info for this model?',
detailResetCoverPreserved: 'No recoverable Civitai backup or original cover was found. The current cover was kept to avoid losing the only image; the custom name and notes were still reset.',
detailResetCoverFailed: 'The cover restore could not be completed. Check file permissions or locks; the custom name and notes were still reset.',
detailResetFailed: 'Reset failed.',
detailCancel: 'Cancel',
detailSaveChanges: 'Save Changes',
detailSaving: 'Saving...',
detailSelectModels: 'Select Models',
detailFolders: 'Folders',
detailSelectedItems: 'Selected {count} items',
detailSelectedItemsPrefix: 'Selected ',
detailSelectedItemsSuffix: ' items',
detailNothingSelected: 'Nothing selected',
detailLoadedModels: 'Loaded {loaded} / {total} models',
detailComputing: 'Computing...',
detailSelectNoPreview: 'Select No Preview',
detailSelectNoDesc: 'Select No Desc',
detailSelectAll: 'Select All',
detailClear: 'Clear',
detailConfirm: 'Confirm',
detailLoading: 'Loading...',
detailAllLoaded: '✅ All models loaded',
detailScrollMore: 'Scroll for more...',
detailNoModels: 'No models found.',
detailApiRestart: '❌ Backend API request failed!\n\nYou must restart the ComfyUI process for the newly written backend code to take effect.',
doctorTitle: 'Global Health Center',
doctorAutoScanOn: 'Auto-Detect on ComfyUI Startup [ON]',
doctorAutoScanOff: 'Auto-Detect on ComfyUI Startup [OFF]',
doctorRefresh: '🔄 Refresh',
doctorRefreshTitle: 'Reload model list and clear errors',
doctorGraphUnavailable: 'The current graph or node is unavailable.',
doctorMissingChainInputs: 'The node does not expose both MODEL and CLIP inputs.',
doctorUnconnectedChainInputs: 'Both MODEL and CLIP inputs must already be connected.',
doctorMissingChainOutputs: 'The node does not expose both MODEL and CLIP outputs.',
doctorAmbiguousBranches: 'Multiple downstream branches were detected; choose an explicit chain first.',
doctorInvalidDownstream: 'An existing downstream link is invalid and cannot be safely changed.',
doctorUnsupportedInsertion: 'This insertion is not available for the selected node.',
doctorCreateLoraFailed: 'Unable to create a standard LoRA Loader node.',
doctorLoraNotReady: 'The LoRA list is not ready. Refresh ComfyUI model lists and try again.',
assistantSelectNode: 'select any node in the canvas',
assistantNoModelParameter: 'No supported model parameter on this node',
assistantSelectedNode: 'Selected node',
assistantQuickActions: 'Quick actions',
assistantChangeWidget: 'Change {name}',
assistantChangeCurrent: 'Change current model',
assistantVisualPicker: '{type} · Visual picker',
assistantInsertBefore: 'Insert LoRA before',
assistantInsertAfter: 'Insert LoRA after',
assistantConnectInputs: 'Connect to node inputs',
assistantConnectOutputs: 'Connect to node outputs',
doctorTotal: 'Total',
doctorHealthy: '🟢 Healthy',
doctorMissing: '🔴 Missing',
doctorNoWorkflowModels: 'No models found in this workflow',
doctorAutoRedirected: 'Auto-Redirected',
doctorReady: 'Ready',
doctorMissingStatus: 'Missing',
doctorCivitai: '🌐 Civitai',
doctorDeepHashScan: '🔍 Deep Hash Scan',
doctorScanStarting: '⏳ Starting scan engine...',
doctorScanning: '⏳ Scanning ({current}/{total}) {filename}',
doctorScanError: '❌ Scan error: ',
doctorMatching: '⏳ Matching and resolving red nodes...',
doctorDeepScanSummary: 'Deep scanned {total} models with missing info.',
doctorNoLocalMatch: '❌ Scan finished, but no local match found.\n\n{summary}\n\nThis means the model is truly missing from your disk, or the original source file with hash was deleted.\nPlease click [🌐 Civitai] to download it from the cloud.',
doctorScanSuccess: '✅ Deep Scan successful! Node auto-healed!\n\n{summary}',
doctorPollError: '❌ Poll Error: ',
doctorScanFailed: '❌ Scan Error: ',
doctorManualReplace: '🔀 Manual Replace',
doctorOriginalPath: 'Original: {path}',
assistantPreviewFailed: 'Failed to load preview',
assistantHistoryTitle: '🖼️ History ({count})',
assistantViewAll: 'View All →',
assistantRestoreWorkflow: 'Click to restore workflow',
assistantViewGallery: 'Click to view full gallery',
pickerNoCompatible: 'No compatible models are available.',
pickerEyebrow: 'Node Assistant · Model Picker',
pickerInsertBeforeTitle: '⬅ Insert LoRA Before Node',
pickerInsertAfterTitle: 'Insert LoRA After Node ➡',
pickerChangeTitle: '🔀 Change Current Model',
pickerFolders: '📁 Folders',
pickerSearch: '🔍 Search name or full path…',
pickerMainAll: 'Main model: All types',
pickerNameAsc: 'Name A–Z',
pickerNameDesc: 'Name Z–A',
pickerByFolder: 'By Folder',
pickerLoadingCovers: '⏳ Loading model covers…',
pickerCancel: 'Cancel',
pickerInsertConfirm: 'Insert & Auto-Connect',
pickerReplaceConfirm: 'Confirm Replacement',
pickerCoversFailed: '⚠️ Covers failed to load; models remain selectable by name.',
doctorNodeLabel: 'Node',
doctorAllHealthy: '🎉 Awesome! All models in this workflow are healthy.',
doctorLoadingPreview: 'Loading preview...',
doctorNoPreview: 'No preview',
doctorProfile: '📖 View Profile',
doctorBase: 'Base',
doctorTriggerWords: 'Trigger Words',
doctorClickCopy: 'Click to copy',
doctorCopyAll: '📋 Copy All',
doctorNotes: '📝 My Notes',
pickerSelected: 'Selected: {path}',
pickerChooseModel: 'Choose a model. Existing connections and strengths will be preserved.',
pickerCompatibleMain: '✨ Compatible LoRAs are filtered for the main model {name}. Use the filter above to show all types.',
pickerCompatibleTag: '✨ No connected main model was identified, so the current LoRA tag {name} is used.',
pickerCompatibleNoTags: 'ℹ Main model {name} was identified, but no matching LoRA tags were found, so all models remain visible.',
pickerCompatibleUnknown: 'ℹ The main model type could not be identified. All LoRAs remain visible and can be filtered manually.',
pickerAllModels: 'All Models',
pickerResults: '{count} results',
pickerCurrent: 'Current',
pickerBaseModel: 'Base model',
pickerRoot: 'Root',
pickerNoMatches: 'No models match the current filters.',
pickerOperationFailed: 'Operation failed: ',
notebookSelectMain: 'Please select a Main Model first.',
closeHelp: 'Close Manual',
delSure: '⚠️ Sure?',
dockTitle: 'Dock to Left',
+95 -69
View File
@@ -4,14 +4,10 @@
*/
import { app } from "../../../scripts/app.js";
import { i18n } from './locales.js';
import { translate } from './locales.js';
import { escapeHtml, setSafeRichHtml } from './safe_dom.js';
const t = (key) => {
let lang = window.anomalous_browser_lang || 'zh';
if (lang.startsWith('en')) lang = 'en';
return (i18n[lang] && i18n[lang][key]) ? i18n[lang][key] : (i18n['zh'][key] || key);
};
const t = (key, params) => translate(key, params);
@@ -84,11 +80,11 @@ export function showDetail(model) {
}
if (isFromDoctor) {
backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回医生面板' : '⬅ Back to Doctor';
backBtn.textContent = t('detailBackDoctor');
backBtn.style.background = '#8AB4F8';
backBtn.style.color = '#000';
} else if (isFromAssistant) {
backBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⬅ 返回助手面板' : '⬅ Back to Assistant';
backBtn.textContent = t('detailBackAssistant');
backBtn.style.background = '#8AB4F8';
backBtn.style.color = '#000';
} else {
@@ -204,7 +200,7 @@ export function showDetail(model) {
const jumpBtn = document.createElement('button');
jumpBtn.innerHTML = '⬇️';
jumpBtn.title = t('jumpToBottom') || 'Jump to bottom';
jumpBtn.title = t('detailJumpToBottom');
jumpBtn.style.padding = '6px 12px';
jumpBtn.style.background = '#444';
jumpBtn.style.color = '#fff';
@@ -224,7 +220,7 @@ export function showDetail(model) {
header.appendChild(jumpBtn);
const applyDetailBtn = document.createElement('button');
applyDetailBtn.innerHTML = t('applyToCanvas') || 'Apply to Canvas';
applyDetailBtn.textContent = t('applyToCanvas');
applyDetailBtn.style.padding = '6px 12px';
applyDetailBtn.style.background = '#007bff';
applyDetailBtn.style.color = '#fff';
@@ -371,7 +367,7 @@ export function showDetail(model) {
}
const editMetaBtn = document.createElement('button');
editMetaBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '⚙️ 编辑' : '⚙️ Edit';
editMetaBtn.textContent = t('detailEdit');
editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto';
editMetaBtn.style.padding = '4px 8px';
editMetaBtn.style.background = '#444';
@@ -413,7 +409,7 @@ export function showDetail(model) {
notesHeader.style.marginBottom = '8px';
const notesTitle = document.createElement('div');
notesTitle.innerHTML = window.anomalous_browser_lang === 'zh' ? '📓 专属备注 (Notes)' : '📓 Custom Notes';
notesTitle.textContent = t('detailNotesTitle');
notesTitle.style.color = '#a38d53';
notesTitle.style.fontWeight = '600';
notesTitle.style.fontSize = '0.85em';
@@ -421,7 +417,7 @@ export function showDetail(model) {
const notesEditBtn = document.createElement('button');
notesEditBtn.innerHTML = '✏️';
notesEditBtn.title = window.anomalous_browser_lang === 'zh' ? '编辑备注' : 'Edit Notes';
notesEditBtn.title = t('detailEditNotes');
notesEditBtn.style.background = 'transparent';
notesEditBtn.style.border = 'none';
notesEditBtn.style.color = '#a38d53';
@@ -472,7 +468,7 @@ export function showDetail(model) {
galleryBtn.style.padding = '10px';
galleryBtn.style.background = '#2a2b2f';
galleryBtn.style.border = '1px solid #3c4043';
galleryBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🖼️ 查看历史生成作品' : '🖼️ View Generated Gallery';
galleryBtn.textContent = t('detailGeneratedGallery');
galleryBtn.onmouseover = () => { galleryBtn.style.background = '#3c4043'; galleryBtn.style.borderColor = '#8ab4f8'; };
galleryBtn.onmouseout = () => { galleryBtn.style.background = '#2a2b2f'; galleryBtn.style.borderColor = '#3c4043'; };
@@ -494,7 +490,7 @@ export function showDetail(model) {
twHeader.style.marginBottom = '5px';
const twLabel = document.createElement('strong');
twLabel.innerText = 'Trained Words:';
twLabel.textContent = t('detailTrainedWords');
twHeader.appendChild(twLabel);
const copyAll = document.createElement('button');
@@ -554,7 +550,7 @@ export function showDetail(model) {
// important for flex scroll
const descLabel = document.createElement('strong');
descLabel.innerText = 'Description:';
descLabel.textContent = t('detailDescription');
descLabel.style.marginBottom = '5px';
descCont.appendChild(descLabel);
@@ -580,7 +576,7 @@ export function showDetail(model) {
notesCont.style.marginTop = '10px';
const notesLabel = document.createElement('strong');
notesLabel.innerText = 'Notes:';
notesLabel.textContent = t('detailNotes');
notesCont.appendChild(notesLabel);
const notesText = document.createElement('div');
@@ -622,7 +618,10 @@ export function showDetail(model) {
window.anomalous_update_hash_cache(d.models);
}
if (!d.models || d.models.length === 0) {
compList.innerHTML = `<span style="color:#888;">No compatible models found.</span>`;
const noCompatible = document.createElement('span');
noCompatible.style.color = '#888';
noCompatible.textContent = t('detailNoCompatibleModels');
compList.replaceChildren(noCompatible);
return;
}
d.models.forEach(m_comp => {
@@ -667,7 +666,10 @@ export function showDetail(model) {
});
})
.catch(e => {
compList.innerHTML = `<span style="color:#ff4444;">Failed to load.</span>`;
const compatibleError = document.createElement('span');
compatibleError.style.color = '#ff4444';
compatibleError.textContent = t('detailCompatibleLoadFailed');
compList.replaceChildren(compatibleError);
});
}
// ---------------------------------
@@ -755,7 +757,7 @@ export function showEditModal(model) {
muteBtn.style.cursor = 'pointer';
muteBtn.style.fontSize = '14px';
muteBtn.style.zIndex = '10';
muteBtn.title = window.anomalous_browser_lang === 'zh' ? '开启/关闭声音' : 'Toggle Sound';
muteBtn.title = t('detailToggleSound');
muteBtn.onclick = (e) => {
e.stopPropagation();
video.muted = !video.muted;
@@ -773,7 +775,12 @@ export function showEditModal(model) {
previewContainer.appendChild(img);
}
} else {
previewContainer.innerHTML = `<div style="color:#9aa0a6; font-size:0.9em; text-align:center;">${window.anomalous_browser_lang === 'zh' ? '暂无封面' : 'No Cover'}</div>`;
const noCover = document.createElement('div');
noCover.style.color = '#9aa0a6';
noCover.style.fontSize = '0.9em';
noCover.style.textAlign = 'center';
noCover.textContent = t('detailNoCover');
previewContainer.appendChild(noCover);
}
const coverRow = document.createElement('div');
@@ -782,7 +789,7 @@ export function showEditModal(model) {
coverRow.style.gap = '8px';
const galleryBtn = document.createElement('button');
galleryBtn.innerHTML = '🖼️ ' + (window.anomalous_browser_lang === 'zh' ? '从历史图库挑选' : 'Pick from Gallery');
galleryBtn.textContent = `🖼️ ${t('detailPickGallery')}`;
galleryBtn.style.padding = '8px';
galleryBtn.style.background = '#303134';
galleryBtn.style.color = '#8ab4f8';
@@ -799,7 +806,7 @@ export function showEditModal(model) {
};
const localBtn = document.createElement('button');
localBtn.innerHTML = '📁 ' + (window.anomalous_browser_lang === 'zh' ? '从本地上传' : 'Upload Local');
localBtn.textContent = `📁 ${t('detailUploadLocal')}`;
localBtn.style.padding = '8px';
localBtn.style.background = '#303134';
localBtn.style.color = '#8ab4f8';
@@ -834,10 +841,10 @@ export function showEditModal(model) {
}
document.body.removeChild(modal);
} else {
alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message);
alert(t('detailUploadError') + data.message);
}
} catch (err) {
alert((window.anomalous_browser_lang === 'zh' ? '上传失败: ' : 'Upload failed: ') + err);
alert(t('detailUploadFailed') + err);
}
}
};
@@ -858,14 +865,17 @@ export function showEditModal(model) {
rightCol.style.gap = '15px';
const title = document.createElement('h2');
title.innerText = window.anomalous_browser_lang === 'zh' ? '编辑模型信息' : 'Edit Model Info';
title.textContent = t('detailModelInfo');
title.style.margin = '0';
title.style.color = '#e8eaed';
title.style.fontSize = '1.25em';
title.style.fontWeight = '500';
const filenameLabel = document.createElement('div');
filenameLabel.innerHTML = `<span style="color:#9aa0a6;">${window.anomalous_browser_lang === 'zh' ? '文件:' : 'File:'}</span> ${escapeHtml(model.filename)}`;
const filenamePrefix = document.createElement('span');
filenamePrefix.style.color = '#9aa0a6';
filenamePrefix.textContent = t('detailFile');
filenameLabel.append(filenamePrefix, document.createTextNode(` ${model.filename}`));
filenameLabel.style.color = '#e8eaed';
filenameLabel.style.fontSize = '0.9em';
filenameLabel.style.wordBreak = 'break-all';
@@ -884,14 +894,14 @@ export function showEditModal(model) {
`;
const nameInput = document.createElement('input');
nameInput.placeholder = window.anomalous_browser_lang === 'zh' ? '自定义名称 (留空则使用原名)' : 'Custom Name (Leave empty to use original)';
nameInput.placeholder = t('detailCustomNamePlaceholder');
nameInput.value = (model.metadata && model.metadata.custom_name) ? model.metadata.custom_name : '';
nameInput.style.cssText = inputStyle;
nameInput.onfocus = () => nameInput.style.borderColor = '#8ab4f8';
nameInput.onblur = () => nameInput.style.borderColor = '#5f6368';
const notesInput = document.createElement('textarea');
notesInput.placeholder = window.anomalous_browser_lang === 'zh' ? '📓 专属备注... (支持多行)' : '📓 Custom Notes...';
notesInput.placeholder = t('detailNotesPlaceholder');
notesInput.value = (model.metadata && model.metadata.custom_notes) ? model.metadata.custom_notes : '';
notesInput.style.cssText = inputStyle;
notesInput.style.flex = '1'; // fill remaining space
@@ -935,7 +945,7 @@ export function showEditModal(model) {
const physicalLabel = document.createElement('label');
physicalLabel.htmlFor = 'anomalous-physical-rename-checkbox';
physicalLabel.innerText = window.anomalous_browser_lang === 'zh' ? '同步物理重命名' : 'Physical Rename on Disk';
physicalLabel.textContent = t('detailPhysicalRename');
physicalLabel.style.color = '#e8eaed';
physicalLabel.style.fontSize = '0.9em';
physicalLabel.style.cursor = 'pointer';
@@ -947,7 +957,7 @@ export function showEditModal(model) {
physicalDesc.style.fontSize = '0.8em';
physicalDesc.style.color = '#9aa0a6';
physicalDesc.style.marginLeft = '22px';
physicalDesc.innerText = window.anomalous_browser_lang === 'zh' ? '若不勾选则仅虚拟重命名,安全不破坏原文件。' : 'Virtual rename only if unchecked. Safe for originals.';
physicalDesc.textContent = t('detailPhysicalRenameDesc');
physicalRow.appendChild(physicalCheckboxWrapper);
physicalRow.appendChild(physicalDesc);
@@ -962,7 +972,7 @@ export function showEditModal(model) {
leftActions.style.gap = '10px';
const resetBtn = document.createElement('button');
resetBtn.innerText = window.anomalous_browser_lang === 'zh' ? '重置设置' : 'Reset All';
resetBtn.textContent = t('detailResetAll');
resetBtn.style.padding = '8px 16px';
resetBtn.style.background = 'transparent';
resetBtn.style.color = '#f28b82';
@@ -971,7 +981,7 @@ export function showEditModal(model) {
resetBtn.style.cursor = 'pointer';
resetBtn.style.fontWeight = '500';
resetBtn.onclick = async () => {
if (!confirm(window.anomalous_browser_lang === 'zh' ? '确定要重置该模型的所有自定义信息(名称、封面、备注)吗?' : 'Reset all custom info for this model?')) return;
if (!confirm(t('detailResetConfirm'))) return;
document.body.removeChild(modal);
try {
const res = await fetch('/anomalous/update_metadata', {
@@ -1002,22 +1012,16 @@ export function showEditModal(model) {
}
if (result.cover_reset === false) {
const noSource = result.cover_reset_source === 'preserved_current';
alert(window.anomalous_browser_lang === 'zh'
? (noSource
? '未找到可恢复的 C 站备份或原始封面。为避免丢失唯一图片,当前封面已保留;自定义名称和备注仍已重置。'
: '封面恢复未能完整完成,请检查文件权限或占用情况;自定义名称和备注仍已重置。')
: (noSource
? 'No recoverable Civitai backup or original cover was found. The current cover was kept to avoid losing the only image; the custom name and notes were still reset.'
: 'The cover restore could not be completed. Check file permissions or locks; the custom name and notes were still reset.'));
alert(t(noSource ? 'detailResetCoverPreserved' : 'detailResetCoverFailed'));
}
} else {
alert(result.message || (window.anomalous_browser_lang === 'zh' ? '重置失败。' : 'Reset failed.'));
alert(result.message || t('detailResetFailed'));
}
} catch (e) { console.error(e); }
};
const cancelBtn = document.createElement('button');
cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel';
cancelBtn.textContent = t('detailCancel');
cancelBtn.style.padding = '8px 16px';
cancelBtn.style.background = 'transparent';
cancelBtn.style.color = '#8ab4f8';
@@ -1031,7 +1035,7 @@ export function showEditModal(model) {
leftActions.appendChild(cancelBtn);
const saveBtn = document.createElement('button');
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存更改' : 'Save Changes';
saveBtn.textContent = t('detailSaveChanges');
saveBtn.style.padding = '8px 24px';
saveBtn.style.background = '#8ab4f8';
saveBtn.style.color = '#202124';
@@ -1042,7 +1046,7 @@ export function showEditModal(model) {
saveBtn.onclick = () => {
const newName = nameInput.value.trim();
const newNotes = notesInput.value.trim();
saveBtn.innerText = window.anomalous_browser_lang === 'zh' ? '保存中...' : 'Saving...';
saveBtn.textContent = t('detailSaving');
saveBtn.disabled = true;
fetch('/anomalous/update_metadata', {
@@ -1071,11 +1075,11 @@ export function showEditModal(model) {
this.showDetail(model);
}
} else {
alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + data.message);
alert(t('detailUploadError') + data.message);
}
}).catch(e => {
document.body.removeChild(modal);
alert((window.anomalous_browser_lang === 'zh' ? '错误: ' : 'Error: ') + e);
alert(t('detailUploadError') + e);
});
};
@@ -1150,7 +1154,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
titleContainer.style.gap = '12px';
const title = document.createElement('h2');
title.innerText = window.anomalous_browser_lang === 'zh' ? '选择模型' : 'Select Models';
title.textContent = t('detailSelectModels');
title.style.margin = '0';
title.style.color = '#e8eaed';
title.style.fontSize = '20px';
@@ -1186,7 +1190,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
leftPanel.style.background = '#171717';
const leftTitle = document.createElement('div');
leftTitle.innerText = window.anomalous_browser_lang === 'zh' ? '目录列表' : 'Folders';
leftTitle.textContent = t('detailFolders');
leftTitle.style.padding = '16px 24px';
leftTitle.style.fontWeight = '500';
leftTitle.style.color = '#9aa0a6';
@@ -1250,11 +1254,18 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
for (const set of localSelection.values()) total += set.size;
if (total > 0) {
summaryText.innerHTML = window.anomalous_browser_lang === 'zh'
? `已选 <span style="color: #8ab4f8; font-size: 16px; font-weight: 500;">${total}</span> 项`
: `Selected <span style="color: #8ab4f8; font-size: 16px; font-weight: 500;">${total}</span> items`;
const countLabel = document.createElement('span');
countLabel.textContent = total;
countLabel.style.color = '#8ab4f8';
countLabel.style.fontSize = '16px';
countLabel.style.fontWeight = '500';
summaryText.replaceChildren(
document.createTextNode(t('detailSelectedItemsPrefix')),
countLabel,
document.createTextNode(t('detailSelectedItemsSuffix')),
);
} else {
summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '暂未选择' : 'Nothing selected';
summaryText.textContent = t('detailNothingSelected');
}
treeContainer.querySelectorAll('.folder-item').forEach(fBtn => {
@@ -1274,9 +1285,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
// Also update total models text in grid
const statusDiv = grid.querySelector('.status-text');
if (statusDiv && !isLoading) {
statusDiv.innerText = window.anomalous_browser_lang === 'zh'
? `已加载 ${currentModels.length} / ${currentTotal} 个模型`
: `Loaded ${currentModels.length} / ${currentTotal} models`;
statusDiv.textContent = t('detailLoadedModels', { loaded: currentModels.length, total: currentTotal });
}
};
@@ -1353,7 +1362,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
return;
}
summaryText.innerHTML = window.anomalous_browser_lang === 'zh' ? '计算中...' : 'Computing...';
summaryText.textContent = t('detailComputing');
try {
const res = await fetch(`/anomalous/batch_select?folderKey=${encodeURIComponent(currentFolderKey)}&action=${action}`);
if (!res.ok) throw new Error('API failed');
@@ -1373,20 +1382,20 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
updateGridCheckboxes();
} catch (e) {
console.error(e);
summaryText.innerHTML = '<span style="color:#f28b82">Error</span>';
summaryText.textContent = t('detailUploadError') + e.message;
setTimeout(() => updateGridCheckboxes(), 2000);
}
};
toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无预览图' : 'Select No Preview', () => handleBatchSelect('no_preview'), true));
toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '选无介绍' : 'Select No Desc', () => handleBatchSelect('no_desc'), true));
toolbar.appendChild(createTBtn(t('detailSelectNoPreview'), () => handleBatchSelect('no_preview'), true));
toolbar.appendChild(createTBtn(t('detailSelectNoDesc'), () => handleBatchSelect('no_desc'), true));
const divi = document.createElement('div');
divi.style.width = '1px'; divi.style.height = '24px'; divi.style.background = '#3c4043'; divi.style.margin = '0 8px';
toolbar.appendChild(divi);
toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '全选' : 'Select All', () => handleBatchSelect('all')));
toolbar.appendChild(createTBtn(window.anomalous_browser_lang === 'zh' ? '清空' : 'Clear', () => handleBatchSelect('none')));
toolbar.appendChild(createTBtn(t('detailSelectAll'), () => handleBatchSelect('all')));
toolbar.appendChild(createTBtn(t('detailClear'), () => handleBatchSelect('none')));
const summaryText = document.createElement('div');
summaryText.style.marginLeft = 'auto';
@@ -1421,7 +1430,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
footer.style.gap = '12px';
const cancelBtn = document.createElement('button');
cancelBtn.innerText = window.anomalous_browser_lang === 'zh' ? '取消' : 'Cancel';
cancelBtn.textContent = t('detailCancel');
cancelBtn.style.padding = '8px 24px';
cancelBtn.style.background = 'transparent';
cancelBtn.style.color = '#8ab4f8';
@@ -1435,7 +1444,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
cancelBtn.onclick = () => document.body.removeChild(modal);
const confirmBtn = document.createElement('button');
confirmBtn.innerText = window.anomalous_browser_lang === 'zh' ? '确定' : 'Confirm';
confirmBtn.textContent = t('detailConfirm');
confirmBtn.style.padding = '8px 24px';
confirmBtn.style.background = '#8ab4f8';
confirmBtn.style.color = '#202124'; // dark text on bright accent button
@@ -1632,7 +1641,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
loadingIndicator.style.textAlign = 'center';
loadingIndicator.style.color = '#9aa0a6';
loadingIndicator.style.fontSize = '16px';
loadingIndicator.innerText = window.anomalous_browser_lang === 'zh' ? '加载中 (Loading)...' : 'Loading...';
loadingIndicator.textContent = t('detailLoading');
grid.appendChild(loadingIndicator);
try {
@@ -1640,7 +1649,16 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
const url = `${currentBaseUrl}${sep}page=${currentPage}&limit=50`;
const res = await fetch(url);
if (!res.ok) {
grid.innerHTML = `<div class="status-text" style="grid-column: 1 / -1; padding: 60px; text-align: center; color: #f28b82; font-size: 16px;">❌ 请求后端 API 失败!<br><br>你必须重启 ComfyUI 进程,刚写入后端的代码才能生效。</div>`;
const apiError = document.createElement('div');
apiError.className = 'status-text';
apiError.style.gridColumn = '1 / -1';
apiError.style.padding = '60px';
apiError.style.textAlign = 'center';
apiError.style.color = '#f28b82';
apiError.style.fontSize = '16px';
apiError.style.whiteSpace = 'pre-line';
apiError.textContent = t('detailApiRestart');
grid.replaceChildren(apiError);
isLoading = false;
return;
}
@@ -1662,7 +1680,15 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
grid.removeChild(loadingIndicator);
if (currentModels.length === 0 && !isLoadMore) {
grid.innerHTML = `<div class="status-text" style="grid-column: 1 / -1; padding: 60px; text-align: center; color: #9aa0a6; font-size: 16px;">No models found.</div>`;
const emptyModels = document.createElement('div');
emptyModels.className = 'status-text';
emptyModels.style.gridColumn = '1 / -1';
emptyModels.style.padding = '60px';
emptyModels.style.textAlign = 'center';
emptyModels.style.color = '#9aa0a6';
emptyModels.style.fontSize = '16px';
emptyModels.textContent = t('detailNoModels');
grid.replaceChildren(emptyModels);
} else {
grid.appendChild(renderCards(fetchedModels));
updateGridCheckboxes();
@@ -1676,9 +1702,9 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
statusText.style.color = '#5f6368';
statusText.style.fontSize = '14px';
if (currentModels.length >= currentTotal) {
statusText.innerText = window.anomalous_browser_lang === 'zh' ? '✅ 已加载全部' : '✅ All models loaded';
statusText.textContent = t('detailAllLoaded');
} else {
statusText.innerText = window.anomalous_browser_lang === 'zh' ? '滑动加载更多...' : 'Scroll for more...';
statusText.textContent = t('detailScrollMore');
}
grid.appendChild(statusText);
}
@@ -1716,7 +1742,7 @@ export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
allBtn.style.display = 'flex';
allBtn.style.justifyContent = 'space-between';
allBtn.style.alignItems = 'center';
allBtn.innerHTML = `<span>🌟 ${window.anomalous_browser_lang === 'zh' ? '全部模型' : 'All Models'}</span>`;
allBtn.textContent = `🌟 ${t('models')}`;
let loadFolder = async (fBtn, folderKey, fetchUrl) => {
treeContainer.querySelectorAll('.folder-item').forEach(d => {
+104 -116
View File
@@ -1,4 +1,5 @@
import { app } from "../../../scripts/app.js";
import { translate } from './locales.js';
import { escapeHtml } from './safe_dom.js';
import {
analyzeModelChainInsertion,
@@ -16,6 +17,8 @@ import {
* Extracted Doctor Panel & Assistant Panel methods.
*/
const t = (key, params) => translate(key, params);
export function initDoctorPanel() {
this.doctorPanelInitialized = true;
this.doctorPanel.innerHTML = '';
@@ -32,7 +35,7 @@ export function initDoctorPanel() {
const titleEl = document.createElement('div');
titleEl.style.cssText = 'display:flex;align-items:center;gap:12px;';
titleEl.innerHTML = `<span style="font-size:24px; filter: drop-shadow(0 0 8px rgba(0,255,204,0.3));">🩺</span><span style="font-size:18px;font-weight:600;color:#fff;font-family:Inter, sans-serif; letter-spacing: 0.5px;">${window.anomalous_browser_lang === 'zh' ? '全局体检中心' : 'Global Health Center'}</span>`;
titleEl.innerHTML = `<span style="font-size:24px; filter: drop-shadow(0 0 8px rgba(0,255,204,0.3));">🩺</span><span style="font-size:18px;font-weight:600;color:#fff;font-family:Inter, sans-serif; letter-spacing: 0.5px;">${t('doctorTitle')}</span>`;
// Header control group on the right side (contains toggle & close button)
const controlGroup = document.createElement('div');
@@ -44,13 +47,9 @@ export function initDoctorPanel() {
const renderAutoScanToggle = () => {
let isAutoEnabled = localStorage.getItem('anomalous_auto_scan_enabled') === 'true';
autoScanToggle.innerHTML = window.anomalous_browser_lang === 'zh'
? (isAutoEnabled
? '<span style="font-size:16px;">🛎️</span><span style="font-size:12px;color:#00ffcc;font-weight:500;">ComfyUI 启动时自动检测并替换缺失模型 [已开启]</span>'
: '<span style="font-size:16px;opacity:0.5;">🔕</span><span style="font-size:12px;color:#aaa;">ComfyUI 启动时自动检测并替换缺失模型 [默认关闭]</span>')
: (isAutoEnabled
? '<span style="font-size:16px;">🛎️</span><span style="font-size:12px;color:#00ffcc;font-weight:500;">Auto-Detect on ComfyUI Startup [ON]</span>'
: '<span style="font-size:16px;opacity:0.5;">🔕</span><span style="font-size:12px;color:#aaa;">Auto-Detect on ComfyUI Startup [OFF]</span>');
autoScanToggle.innerHTML = isAutoEnabled
? `<span style="font-size:16px;">🛎️</span><span style="font-size:12px;color:#00ffcc;font-weight:500;">${t('doctorAutoScanOn')}</span>`
: `<span style="font-size:16px;opacity:0.5;">🔕</span><span style="font-size:12px;color:#aaa;">${t('doctorAutoScanOff')}</span>`;
};
renderAutoScanToggle();
@@ -63,9 +62,9 @@ export function initDoctorPanel() {
};
const refreshBtn = document.createElement('button');
refreshBtn.innerHTML = window.anomalous_browser_lang === 'zh' ? '🔄 刷新缓存' : '🔄 Refresh';
refreshBtn.textContent = t('doctorRefresh');
refreshBtn.style.cssText = 'background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.3);color:#8AB4F8;font-size:12px;cursor:pointer;padding:6px 12px;border-radius:6px;transition:all 0.2s; font-weight:600;';
refreshBtn.title = window.anomalous_browser_lang === 'zh' ? '重新读取最新模型列表并清除报错' : 'Reload model list and clear errors';
refreshBtn.title = t('doctorRefreshTitle');
refreshBtn.onmouseover = () => { refreshBtn.style.background = 'rgba(138,180,248,0.2)'; };
refreshBtn.onmouseout = () => { refreshBtn.style.background = 'rgba(138,180,248,0.1)'; };
refreshBtn.onclick = async () => {
@@ -119,16 +118,15 @@ export function initDoctorPanel() {
}
function getInsertionCapabilityMessage(capability) {
const zh = window.anomalous_browser_lang === 'zh';
const messages = {
missing_graph_or_node: zh ? '当前画布或节点不可用。' : 'The current graph or node is unavailable.',
missing_chain_inputs: zh ? '节点没有完整的 MODEL/CLIP 输入。' : 'The node does not expose both MODEL and CLIP inputs.',
unconnected_chain_inputs: zh ? 'MODEL/CLIP 输入尚未全部连接。' : 'Both MODEL and CLIP inputs must already be connected.',
missing_chain_outputs: zh ? '节点没有完整的 MODEL/CLIP 输出。' : 'The node does not expose both MODEL and CLIP outputs.',
ambiguous_downstream_branches: zh ? '检测到多个下游分支,请先整理或选择明确链路。' : 'Multiple downstream branches were detected; choose an explicit chain first.',
invalid_downstream_link: zh ? '现有下游连线无效,无法安全改写。' : 'An existing downstream link is invalid and cannot be safely changed.',
missing_graph_or_node: 'doctorGraphUnavailable',
missing_chain_inputs: 'doctorMissingChainInputs',
unconnected_chain_inputs: 'doctorUnconnectedChainInputs',
missing_chain_outputs: 'doctorMissingChainOutputs',
ambiguous_downstream_branches: 'doctorAmbiguousBranches',
invalid_downstream_link: 'doctorInvalidDownstream',
};
return messages[capability?.code] || (zh ? '当前节点不支持此插入方式。' : 'This insertion is not available for the selected node.');
return t(messages[capability?.code] || 'doctorUnsupportedInsertion');
}
function getNativeWidgetValues(node, widget) {
@@ -175,13 +173,13 @@ export function openLoraInsertionPicker(anchorNode, direction) {
const insertedNode = typeof LiteGraph !== 'undefined' ? LiteGraph.createNode('LoraLoader') : null;
if (!insertedNode) {
alert(window.anomalous_browser_lang === 'zh' ? '无法创建标准 LoRA Loader 节点。' : 'Unable to create a standard LoRA Loader node.');
alert(t('doctorCreateLoraFailed'));
return;
}
const modelWidget = findModelComboWidget(insertedNode);
if (!modelWidget || getNativeWidgetValues(insertedNode, modelWidget).length === 0) {
alert(window.anomalous_browser_lang === 'zh' ? 'LoRA 列表尚未就绪,请刷新 ComfyUI 模型列表后重试。' : 'The LoRA list is not ready. Refresh ComfyUI model lists and try again.');
alert(t('doctorLoraNotReady'));
return;
}
@@ -204,7 +202,7 @@ if (!this.assistantPanelInitialized) {
if (!placeholder || !nodeContent || !app.graph || !app.graph._nodes) return;
if (!node) {
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div style="text-align:center;">${window.anomalous_browser_lang === 'zh' ? '请在画布中<strong style="color:#aaa">点击选中任意节点</strong>' : 'Please <strong style="color:#aaa">select any node</strong> in the canvas'}</div>`;
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div style="text-align:center;">${t('assistantSelectNode')}</div>`;
placeholder.style.display = 'flex';
nodeContent.style.display = 'none';
nodeContent.innerHTML = '';
@@ -224,7 +222,7 @@ for (const w of node.widgets) {
const canInsert = insertionCapabilities.before.supported || insertionCapabilities.after.supported;
if (modelWidgets.length === 0 && !canInsert) {
placeholder.innerHTML = `<div style="font-size:36px;">⚠️</div><div style="text-align:center;">${window.anomalous_browser_lang === 'zh' ? '该节点没有受支持的模型参数' : 'No supported model parameter on this node'}</div><div style="font-size:12px;color:#555;margin-top:4px;">${escapeHtml(node.type || '')}</div>`;
placeholder.innerHTML = `<div style="font-size:36px;">⚠️</div><div style="text-align:center;">${t('assistantNoModelParameter')}</div><div style="font-size:12px;color:#555;margin-top:4px;">${escapeHtml(node.type || '')}</div>`;
placeholder.style.display = 'flex';
nodeContent.style.display = 'none';
nodeContent.innerHTML = '';
@@ -237,7 +235,7 @@ for (const w of node.widgets) {
const titleBar = document.createElement('div');
titleBar.style.cssText = 'margin:14px 16px 0;padding:16px;border:1px solid rgba(138,180,248,0.16);border-radius:14px;background:linear-gradient(135deg,rgba(48,58,86,0.72),rgba(20,22,31,0.92));display:flex;align-items:center;gap:12px;flex-shrink:0;box-shadow:0 12px 30px rgba(0,0,0,0.18);';
titleBar.innerHTML = `<span style="width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:20px;background:linear-gradient(135deg,#8ab4f8,#7c4dff);box-shadow:0 6px 16px rgba(66,133,244,0.3);">🤖</span><span style="display:flex;flex-direction:column;min-width:0;gap:3px;"><span style="font-size:10px;letter-spacing:0.11em;text-transform:uppercase;color:#8ab4f8;">${window.anomalous_browser_lang === 'zh' ? '当前选中节点' : 'Selected node'}</span><span class="ast-title" style="font-weight:750;color:#fff;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span></span><span class="ast-type" style="font-size:10px;color:#c9d6ff;margin-left:auto;padding:5px 8px;border-radius:999px;border:1px solid rgba(138,180,248,0.24);background:rgba(138,180,248,0.08);max-width:38%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span>`;
titleBar.innerHTML = `<span style="width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:20px;background:linear-gradient(135deg,#8ab4f8,#7c4dff);box-shadow:0 6px 16px rgba(66,133,244,0.3);">🤖</span><span style="display:flex;flex-direction:column;min-width:0;gap:3px;"><span style="font-size:10px;letter-spacing:0.11em;text-transform:uppercase;color:#8ab4f8;">${t('assistantSelectedNode')}</span><span class="ast-title" style="font-weight:750;color:#fff;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span></span><span class="ast-type" style="font-size:10px;color:#c9d6ff;margin-left:auto;padding:5px 8px;border-radius:999px;border:1px solid rgba(138,180,248,0.24);background:rgba(138,180,248,0.08);max-width:38%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span>`;
titleBar.querySelector('.ast-title').textContent = node.title || node.type || 'Node';
titleBar.querySelector('.ast-type').textContent = node.type || '';
nodeContent.appendChild(titleBar);
@@ -245,7 +243,7 @@ for (const w of node.widgets) {
const quickActions = document.createElement('div');
quickActions.style.cssText = 'padding:14px 16px 4px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;flex-shrink:0;';
const actionsLabel = document.createElement('div');
actionsLabel.textContent = window.anomalous_browser_lang === 'zh' ? '快捷操作' : 'Quick actions';
actionsLabel.textContent = t('assistantQuickActions');
actionsLabel.style.cssText = 'grid-column:1/-1;color:#8b91a3;font-size:10px;font-weight:750;letter-spacing:0.1em;text-transform:uppercase;padding:0 2px 2px;';
quickActions.appendChild(actionsLabel);
@@ -282,13 +280,13 @@ for (const w of node.widgets) {
for (const widget of modelWidgets) {
const widgetLabel = modelWidgets.length > 1 && widget.name
? (window.anomalous_browser_lang === 'zh' ? `更换 ${widget.name}` : `Change ${widget.name}`)
: (window.anomalous_browser_lang === 'zh' ? '更换当前模型' : 'Change current model');
? t('assistantChangeWidget', { name: widget.name })
: t('assistantChangeCurrent');
const pickerType = inferPickerModelType(node, widget);
makeActionButton({
icon: '⇄',
label: widgetLabel,
hint: window.anomalous_browser_lang === 'zh' ? `${pickerType.label} · 可视化选择` : `${pickerType.label} · Visual picker`,
hint: t('assistantVisualPicker', { type: pickerType.label }),
accent: 'linear-gradient(135deg,rgba(245,124,0,0.96),rgba(255,82,82,0.82))',
onClick: () => this._openGalleryReplacer(node, widget),
primary: true,
@@ -296,16 +294,16 @@ for (const w of node.widgets) {
}
makeActionButton({
icon: '←',
label: window.anomalous_browser_lang === 'zh' ? '前方插入 LoRA' : 'Insert LoRA before',
hint: window.anomalous_browser_lang === 'zh' ? '接入节点输入端' : 'Connect to node inputs',
label: t('assistantInsertBefore'),
hint: t('assistantConnectInputs'),
accent: 'linear-gradient(135deg,rgba(25,118,210,0.9),rgba(80,110,230,0.82))',
onClick: () => this.openLoraInsertionPicker(node, 'before'),
capability: insertionCapabilities.before,
});
makeActionButton({
icon: '→',
label: window.anomalous_browser_lang === 'zh' ? '后方插入 LoRA' : 'Insert LoRA after',
hint: window.anomalous_browser_lang === 'zh' ? '接入节点输出端' : 'Connect to node outputs',
label: t('assistantInsertAfter'),
hint: t('assistantConnectOutputs'),
accent: 'linear-gradient(135deg,rgba(0,137,123,0.92),rgba(67,160,71,0.82))',
onClick: () => this.openLoraInsertionPicker(node, 'after'),
capability: insertionCapabilities.after,
@@ -392,17 +390,16 @@ export function renderGlobalDashboard() {
<span style="color:#fff;font-size:14px;font-weight:bold;">${count}</span>
</div>`;
};
const zh = window.anomalous_browser_lang === 'zh';
statsRow.innerHTML = `
${createBadge(zh?'总模型':'Total', total, '#aaa', 'rgba(255,255,255,0.05)')}
${createBadge(zh?'🟢 健康':'🟢 Healthy', healthy, '#28a745', 'rgba(40, 167, 69, 0.1)')}
${createBadge(zh?'🔴 缺失':'🔴 Missing', missing, '#ff6b6b', 'rgba(220, 53, 69, 0.1)')}
${createBadge(t('doctorTotal'), total, '#aaa', 'rgba(255,255,255,0.05)')}
${createBadge(t('doctorHealthy'), healthy, '#28a745', 'rgba(40, 167, 69, 0.1)')}
${createBadge(t('doctorMissing'), missing, '#ff6b6b', 'rgba(220, 53, 69, 0.1)')}
`;
if (total === 0) {
content.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:rgba(255,255,255,0.3);font-size:14px;">
<div style="font-size:48px;margin-bottom:16px;">👻</div>
${zh ? '当前工作流没有任何模型节点' : 'No models found in this workflow'}
${t('doctorNoWorkflowModels')}
</div>`;
return;
}
@@ -443,11 +440,11 @@ for (const data of missingNodesData) {
right.style.cssText = 'display:flex; align-items:center; gap:12px;';
if (isHealthy && exactMatch && exactMatch !== val) {
right.innerHTML = `<div style="text-align:right;"><div style="color:#ffc107;font-size:13px;font-weight:bold;">🟡 ${zh?'自动重定向':'Auto-Redirected'}</div><div style="color:rgba(255,255,255,0.4);font-size:11px;margin-top:4px;">${escapeHtml(exactMatch.split(/[\/]/).pop())}</div></div>`;
right.innerHTML = `<div style="text-align:right;"><div style="color:#ffc107;font-size:13px;font-weight:bold;">🟡 ${t('doctorAutoRedirected')}</div><div style="color:rgba(255,255,255,0.4);font-size:11px;margin-top:4px;">${escapeHtml(exactMatch.split(/[\/]/).pop())}</div></div>`;
} else if (isHealthy) {
right.innerHTML = `<div style="color:#28a745;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(40,167,69,0.1);border-radius:20px;">🟢 ${zh?'正常':'Ready'}</div>`;
right.innerHTML = `<div style="color:#28a745;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(40,167,69,0.1);border-radius:20px;">🟢 ${t('doctorReady')}</div>`;
} else {
right.innerHTML = `<div style="color:#ff6b6b;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(220,53,69,0.1);border-radius:20px;">🔴 ${zh?'丢失':'Missing'}</div>`;
right.innerHTML = `<div style="color:#ff6b6b;font-size:13px;font-weight:bold;padding:6px 12px;background:rgba(220,53,69,0.1);border-radius:20px;">🔴 ${t('doctorMissingStatus')}</div>`;
}
top.appendChild(left);
@@ -458,7 +455,7 @@ for (const data of missingNodesData) {
actionRow.style.cssText = 'display:flex; gap:10px; margin-top:16px; margin-left:8px;';
const civitaiBtn = document.createElement('button');
civitaiBtn.innerHTML = zh ? '🌐 C站' : '🌐 Civitai';
civitaiBtn.textContent = t('doctorCivitai');
civitaiBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
civitaiBtn.onmouseover = () => civitaiBtn.style.background = 'rgba(255,255,255,0.2)';
civitaiBtn.onmouseout = () => civitaiBtn.style.background = 'rgba(255,255,255,0.1)';
@@ -480,12 +477,12 @@ for (const data of missingNodesData) {
if (!isHealthy) {
const deepScanBtn = document.createElement('button');
deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan';
deepScanBtn.textContent = t('doctorDeepHashScan');
deepScanBtn.style.cssText = 'padding:8px 16px; background:#1a73e8; color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
deepScanBtn.onmouseover = () => deepScanBtn.style.background = '#1557b0';
deepScanBtn.onmouseout = () => deepScanBtn.style.background = '#1a73e8';
deepScanBtn.onclick = async () => {
deepScanBtn.innerText = zh ? '⏳ 正在启动扫描引擎...' : '⏳ Starting scan engine...';
deepScanBtn.textContent = t('doctorScanStarting');
deepScanBtn.disabled = true;
deepScanBtn.style.opacity = '0.7';
@@ -509,20 +506,18 @@ if (!isHealthy) {
if (statusData.scanning) {
let filename = statusData.filename || '';
if (filename.length > 20) filename = filename.substring(0, 10) + '...' + filename.substring(filename.length - 7);
deepScanBtn.innerText = zh
? `⏳ 扫描中 (${statusData.current}/${statusData.total}) ${filename}`
: `⏳ Scanning (${statusData.current}/${statusData.total}) ${filename}`;
deepScanBtn.textContent = t('doctorScanning', { current: statusData.current, total: statusData.total, filename });
setTimeout(pollStatus, 500);
} else {
if (statusData.error) {
alert(zh ? '❌ 扫描过程中发生错误: ' + statusData.error : '❌ Scan error: ' + statusData.error);
deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan';
alert(t('doctorScanError') + statusData.error);
deepScanBtn.textContent = t('doctorDeepHashScan');
deepScanBtn.disabled = false;
deepScanBtn.style.opacity = '1';
return;
}
deepScanBtn.innerText = zh ? '⏳ 正在匹配并替换飘红节点...' : '⏳ Matching and resolving red nodes...';
deepScanBtn.textContent = t('doctorMatching');
if (window.anomalous_reload_hashes) {
await window.anomalous_reload_hashes();
@@ -542,24 +537,22 @@ if (window.anomalous_resolve_all_missing_nodes) {
}
}
const scanInfo = zh
? `共深度扫描了 ${statusData.total} 个缺失信息的模型。`
: `Deep scanned ${statusData.total} models with missing info.`;
const scanInfo = t('doctorDeepScanSummary', { total: statusData.total });
if (stillMissing) {
alert(zh ? `❌ 扫描结束,本地未匹配到模型。\n\n${scanInfo}\n\n这说明模型可能真的不在您的硬盘里,或者您删除了原本记录着哈希的源文件。\n请点击卡片上的【🌐 C站】去云端下载。` : `❌ Scan finished, but no local match found.\n\n${scanInfo}\n\nThis means the model is truly missing from your disk, or the original source file with hash was deleted.\nPlease click [🌐 Civitai] to download it from the cloud.`);
alert(t('doctorNoLocalMatch', { summary: scanInfo }));
} else {
alert(zh ? `✅ 深度扫描成功!已自动修复该节点!\n\n${scanInfo}` : `✅ Deep Scan successful! Node auto-healed!\n\n${scanInfo}`);
alert(t('doctorScanSuccess', { summary: scanInfo }));
}
this.renderGlobalDashboard();
deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan';
deepScanBtn.textContent = t('doctorDeepHashScan');
deepScanBtn.disabled = false;
deepScanBtn.style.opacity = '1';
}
} catch (err) {
alert(zh ? '❌ 状态轮询出错: ' + err.message : '❌ Poll Error: ' + err.message);
deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan';
alert(t('doctorPollError') + err.message);
deepScanBtn.textContent = t('doctorDeepHashScan');
deepScanBtn.disabled = false;
deepScanBtn.style.opacity = '1';
}
@@ -567,15 +560,15 @@ if (stillMissing) {
setTimeout(pollStatus, 500);
} catch(e) {
alert(zh ? '❌ 扫描出错: ' + e.message : '❌ Scan Error: ' + e.message);
deepScanBtn.innerHTML = zh ? '🔍 深度哈希扫描' : '🔍 Deep Hash Scan';
alert(t('doctorScanFailed') + e.message);
deepScanBtn.textContent = t('doctorDeepHashScan');
deepScanBtn.disabled = false;
deepScanBtn.style.opacity = '1';
}
};
const manualBtn = document.createElement('button');
manualBtn.innerHTML = zh ? '🔀 手动替换' : '🔀 Manual Replace';
manualBtn.textContent = t('doctorManualReplace');
manualBtn.style.cssText = 'padding:8px 16px; background:rgba(255,255,255,0.1); color:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:background 0.2s;';
manualBtn.onmouseover = () => manualBtn.style.background = 'rgba(255,255,255,0.2)';
manualBtn.onmouseout = () => manualBtn.style.background = 'rgba(255,255,255,0.1)';
@@ -587,7 +580,7 @@ if (stillMissing) {
actionRow.appendChild(manualBtn);
const pathText = document.createElement('div');
pathText.innerText = `${zh?'原路径':'Original'}: ${val}`;
pathText.textContent = t('doctorOriginalPath', { path: val });
pathText.style.cssText = 'margin-top:12px; margin-left:8px; color:rgba(255,255,255,0.3); font-size:11px; font-family:monospace; word-break:break-all;';
item.appendChild(pathText);
}
@@ -637,7 +630,7 @@ if (!this._assistantPanelHooked) {
const placeholder = document.createElement('div');
placeholder.id = 'anomalous-assistant-placeholder';
placeholder.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;color:#666;font-size:15px;gap:12px;padding:40px;text-align:center;';
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div>${window.anomalous_browser_lang === 'zh' ? '请在画布中<strong style="color:#aaa">点击选中任意节点</strong>' : 'Please <strong style="color:#aaa">select any node</strong> in the canvas'}</div>`;
placeholder.innerHTML = `<div style="font-size:48px;">🤖</div><div>${t('assistantSelectNode')}</div>`;
this.assistantPanel.appendChild(placeholder);
const nodeContent = document.createElement('div');
@@ -657,7 +650,7 @@ export function renderAssistantModelCard(node, w, container) {
// Preview image
const previewBox = document.createElement('div');
previewBox.style.cssText = 'width:100%;aspect-ratio:1.65;max-height:260px;background:radial-gradient(circle at 50% 20%,#252a3b,#0b0c10 70%);border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(255,255,255,0.05);';
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '加载预览...' : 'Loading preview...'}</span>`;
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('doctorLoadingPreview')}</span>`;
wrapper.appendChild(previewBox);
// Name and path
@@ -689,7 +682,7 @@ export function renderAssistantModelCard(node, w, container) {
const actionRow = document.createElement('div');
actionRow.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;';
const profileBtn = document.createElement('button');
profileBtn.innerText = window.anomalous_browser_lang === 'zh' ? '📖 查看档案' : '📖 View Profile';
profileBtn.textContent = t('doctorProfile');
profileBtn.style.cssText = 'flex:1;padding:10px 12px;background:rgba(138,180,248,0.09);color:#a9c7ff;border:1px solid rgba(138,180,248,0.22);border-radius:9px;cursor:pointer;font-size:12px;font-weight:700;transition:filter 0.2s;';
profileBtn.onmouseover = () => profileBtn.style.filter = 'brightness(1.2)';
profileBtn.onmouseout = () => profileBtn.style.filter = 'brightness(1)';
@@ -719,7 +712,7 @@ if (isVid) {
previewBox.appendChild(img);
}
} else {
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '暂无预览图' : 'No preview'}</span>`;
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('doctorNoPreview')}</span>`;
}
// Profile button links to detail
@@ -747,7 +740,7 @@ if (isVid) {
badgeRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;';
const b = document.createElement('span');
b.style.cssText = 'background:rgba(0,255,204,0.08);border:1px solid rgba(0,255,204,0.15);color:#77e6cf;padding:4px 8px;border-radius:999px;font-size:10px;';
b.innerText = `${window.anomalous_browser_lang === 'zh' ? '基础模型' : 'Base'} · ${meta.baseModel}`;
b.textContent = `${t('doctorBase')} · ${meta.baseModel}`;
badgeRow.appendChild(b);
metaZone.appendChild(badgeRow);
}
@@ -759,7 +752,7 @@ if (triggers && triggers.length > 0) {
trigSection.style.cssText = 'background:rgba(255,255,255,0.04);border-radius:6px;padding:10px 12px;';
const trigTitle = document.createElement('div');
trigTitle.style.cssText = 'color:#aaa;font-size:11px;margin-bottom:8px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
trigTitle.innerText = window.anomalous_browser_lang === 'zh' ? '触发词' : 'Trigger Words';
trigTitle.textContent = t('doctorTriggerWords');
const tagList = document.createElement('div');
tagList.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;';
const words = Array.isArray(triggers) ? triggers : [triggers];
@@ -767,7 +760,7 @@ if (triggers && triggers.length > 0) {
const tag = document.createElement('span');
tag.style.cssText = 'background:rgba(255,193,7,0.12);color:#ffc107;padding:3px 8px;border-radius:4px;font-size:12px;cursor:pointer;';
tag.innerText = word;
tag.title = window.anomalous_browser_lang === 'zh' ? '点击复制' : 'Click to copy';
tag.title = t('doctorClickCopy');
tag.onclick = () => {
navigator.clipboard.writeText(word).then(() => { const o = tag.innerText; tag.innerText = '✅'; setTimeout(() => tag.innerText = o, 1000); });
};
@@ -775,9 +768,9 @@ if (triggers && triggers.length > 0) {
});
const copyAll = document.createElement('button');
copyAll.style.cssText = 'background:transparent;border:1px solid #444;color:#888;border-radius:4px;padding:3px 8px;font-size:11px;cursor:pointer;margin-top:4px;';
copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '📋 全部复制' : '📋 Copy All';
copyAll.textContent = t('doctorCopyAll');
copyAll.onclick = () => {
navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.innerText = '✅'; setTimeout(() => copyAll.innerText = window.anomalous_browser_lang === 'zh' ? '📋 全部复制' : '📋 Copy All', 1500); });
navigator.clipboard.writeText(words.join(', ')).then(() => { copyAll.textContent = '✅'; setTimeout(() => copyAll.textContent = t('doctorCopyAll'), 1500); });
};
trigSection.appendChild(trigTitle);
trigSection.appendChild(tagList);
@@ -792,7 +785,7 @@ if (textNotes) {
notesCard.style.cssText = 'background:linear-gradient(135deg,#262522 0%,#202124 100%);border:1px solid #3c4043;border-left:4px solid #a38d53;border-radius:4px 8px 8px 4px;padding:12px 14px;';
const notesTitle = document.createElement('div');
notesTitle.style.cssText = 'color:#a38d53;font-size:11px;font-weight:bold;margin-bottom:6px;';
notesTitle.innerText = window.anomalous_browser_lang === 'zh' ? '📝 我的备注' : '📝 My Notes';
notesTitle.textContent = t('doctorNotes');
const notesText = document.createElement('div');
notesText.style.cssText = 'color:#d4c4a0;font-size:13px;line-height:1.6;white-space:pre-wrap;';
notesText.innerText = textNotes;
@@ -806,7 +799,7 @@ if (textNotes) {
const resolvedFilename = (d.status === 'success' && d.model) ? (d.model.filename || filename) : filename;
this._loadAssistantHistory(resolvedFilename, metaZone, d.status === 'success' ? d.model : null);
}).catch(() => {
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${window.anomalous_browser_lang === 'zh' ? '无法加载预览' : 'Failed to load preview'}</span>`;
previewBox.innerHTML = `<span style="color:#444;font-size:13px;">${t('assistantPreviewFailed')}</span>`;
// Still try to load history gallery by filename
this._loadAssistantHistory(filename, metaZone, null);
@@ -828,13 +821,13 @@ export function _loadAssistantHistory(filename, container, model) {
sectionHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;';
const sectionTitle = document.createElement('div');
sectionTitle.style.cssText = 'color:#aaa;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:0.5px;';
sectionTitle.innerText = window.anomalous_browser_lang === 'zh' ? `🖼️ 历史生成图 (${images.length})` : `🖼️ History (${images.length})`;
sectionTitle.textContent = t('assistantHistoryTitle', { count: images.length });
sectionHeader.appendChild(sectionTitle);
// "View all" button if model is available
if (model) {
const viewAllBtn = document.createElement('button');
viewAllBtn.innerText = window.anomalous_browser_lang === 'zh' ? '查看全部 →' : 'View All →';
viewAllBtn.textContent = t('assistantViewAll');
viewAllBtn.style.cssText = 'background:transparent;border:1px solid rgba(138,180,248,0.3);color:#8AB4F8;font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer;transition:all 0.2s;';
viewAllBtn.onmouseover = () => { viewAllBtn.style.background = 'rgba(138,180,248,0.1)'; };
viewAllBtn.onmouseout = () => { viewAllBtn.style.background = 'transparent'; };
@@ -871,7 +864,7 @@ if (model) {
card.appendChild(imgEl);
}
if (img.workflow) {
card.title = window.anomalous_browser_lang === 'zh' ? '点击恢复此工作流' : 'Click to restore workflow';
card.title = t('assistantRestoreWorkflow');
card.onclick = () => {
try {
const wf = typeof img.workflow === 'string' ? JSON.parse(img.workflow) : img.workflow;
@@ -879,7 +872,7 @@ if (img.workflow) {
} catch (e) { }
};
} else if (model) {
card.title = window.anomalous_browser_lang === 'zh' ? '点击查看完整图库' : 'Click to view full gallery';
card.title = t('assistantViewGallery');
card.onclick = () => this.showGeneratedGallery(model);
}
grid.appendChild(card);
@@ -890,12 +883,11 @@ if (img.workflow) {
}
export function _openGalleryReplacer(node, w, options = {}) {
const zh = window.anomalous_browser_lang === 'zh';
const mode = options.mode === 'insert' ? 'insert' : 'replace';
const pickerType = inferPickerModelType(node, w, options);
const validPaths = getNativeWidgetValues(node, w);
if (!validPaths.length) {
alert(zh ? '没有可供选择的兼容模型。' : 'No compatible models are available.');
alert(t('pickerNoCompatible'));
return;
}
@@ -942,15 +934,13 @@ export function _openGalleryReplacer(node, w, options = {}) {
const headerCopy = document.createElement('div');
headerCopy.style.cssText = 'display:flex;flex-direction:column;gap:3px;';
const eyebrow = document.createElement('div');
eyebrow.textContent = zh ? '节点助手 · 模型选择器' : 'Node Assistant · Model Picker';
eyebrow.textContent = t('pickerEyebrow');
eyebrow.style.cssText = 'font-size:10px;color:#8fa8da;letter-spacing:0.12em;text-transform:uppercase;font-weight:750;';
const title = document.createElement('h2');
title.style.cssText = 'margin:0;font-size:19px;line-height:1.15;';
title.textContent = mode === 'insert'
? (options.direction === 'before'
? (zh ? '⬅ 在节点前方插入 LoRA' : '⬅ Insert LoRA Before Node')
: (zh ? '在节点后方插入 LoRA ➡' : 'Insert LoRA After Node ➡'))
: (zh ? '🔀 更换当前模型' : '🔀 Change Current Model');
? (options.direction === 'before' ? t('pickerInsertBeforeTitle') : t('pickerInsertAfterTitle'))
: t('pickerChangeTitle');
const typeBadge = document.createElement('span');
typeBadge.textContent = pickerType.label;
typeBadge.style.cssText = 'padding:5px 10px;border-radius:20px;background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.25);color:#a9c7ff;font-size:10px;font-weight:750;';
@@ -971,7 +961,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
const folderPanel = document.createElement('aside');
folderPanel.style.cssText = 'background:rgba(20,22,29,0.92);border:1px solid rgba(255,255,255,0.075);border-radius:13px;overflow:auto;padding:10px;box-shadow:0 14px 35px rgba(0,0,0,0.18);';
const folderTitle = document.createElement('div');
folderTitle.textContent = zh ? '📁 文件夹' : '📁 Folders';
folderTitle.textContent = t('pickerFolders');
folderTitle.style.cssText = 'font-size:13px;font-weight:700;color:#ddd;padding:8px 10px 10px;';
const folderList = document.createElement('div');
folderList.style.cssText = 'display:flex;flex-direction:column;gap:3px;';
@@ -983,20 +973,20 @@ export function _openGalleryReplacer(node, w, options = {}) {
toolbar.style.cssText = 'display:flex;gap:10px;padding:12px;border-bottom:1px solid #333;flex-wrap:wrap;align-items:center;';
const searchInput = document.createElement('input');
searchInput.type = 'search';
searchInput.placeholder = zh ? '🔍 搜索名称或完整路径…' : '🔍 Search name or full path…';
searchInput.placeholder = t('pickerSearch');
searchInput.style.cssText = 'flex:1;min-width:220px;padding:10px 12px;border-radius:9px;border:1px solid rgba(255,255,255,0.1);background:#222631;color:#fff;font-size:13px;outline:none;';
const baseFilterSelect = document.createElement('select');
baseFilterSelect.style.cssText = `display:${pickerType.isLora ? 'block' : 'none'};padding:10px 12px;border-radius:9px;border:1px solid rgba(0,220,180,0.2);background:#1d292b;color:#9be8d9;font-size:12px;max-width:230px;`;
const allBaseOption = document.createElement('option');
allBaseOption.value = '';
allBaseOption.textContent = zh ? '主模型:全部类型' : 'Main model: All types';
allBaseOption.textContent = t('pickerMainAll');
baseFilterSelect.appendChild(allBaseOption);
const sortSelect = document.createElement('select');
sortSelect.style.cssText = 'padding:10px 12px;border-radius:9px;border:1px solid rgba(255,255,255,0.1);background:#222631;color:#fff;font-size:12px;';
[
['name-asc', zh ? '名称 A–Z' : 'Name A–Z'],
['name-desc', zh ? '名称 Z–A' : 'Name Z–A'],
['folder-asc', zh ? '按文件夹' : 'By Folder'],
['name-asc', t('pickerNameAsc')],
['name-desc', t('pickerNameDesc')],
['folder-asc', t('pickerByFolder')],
].forEach(([value, label]) => {
const option = document.createElement('option');
option.value = value;
@@ -1009,7 +999,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
content.appendChild(toolbar);
const loadingText = document.createElement('div');
loadingText.textContent = zh ? '⏳ 正在加载模型封面…' : '⏳ Loading model covers…';
loadingText.textContent = t('pickerLoadingCovers');
loadingText.style.cssText = 'color:#888;font-size:13px;padding:10px 14px 0;';
const gridScroll = document.createElement('div');
gridScroll.style.cssText = 'overflow:auto;min-height:0;flex:1;padding:14px;';
@@ -1025,13 +1015,13 @@ export function _openGalleryReplacer(node, w, options = {}) {
const selectionText = document.createElement('div');
selectionText.style.cssText = 'min-width:0;flex:1;color:#aaa;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = zh ? '取消' : 'Cancel';
cancelBtn.textContent = t('pickerCancel');
cancelBtn.style.cssText = 'padding:10px 18px;background:#333;color:#ddd;border:1px solid #555;border-radius:7px;cursor:pointer;';
cancelBtn.onclick = closeModal;
const confirmBtn = document.createElement('button');
confirmBtn.textContent = mode === 'insert'
? (zh ? '插入并自动连线' : 'Insert & Auto-Connect')
: (zh ? '确认替换' : 'Confirm Replacement');
? t('pickerInsertConfirm')
: t('pickerReplaceConfirm');
confirmBtn.style.cssText = 'padding:10px 20px;background:#1976d2;color:#fff;border:none;border-radius:7px;cursor:pointer;font-weight:700;';
footer.append(selectionText, cancelBtn, confirmBtn);
modal.appendChild(footer);
@@ -1059,8 +1049,8 @@ export function _openGalleryReplacer(node, w, options = {}) {
const updateSelection = () => {
selectionText.textContent = selectedPath
? `${zh ? '已选择' : 'Selected'}: ${normalizePath(selectedPath)}`
: (zh ? '请选择一个模型。节点连接和强度不会被改动。' : 'Choose a model. Existing connections and strengths will be preserved.');
? t('pickerSelected', { path: normalizePath(selectedPath) })
: t('pickerChooseModel');
confirmBtn.disabled = !selectedPath || applying;
confirmBtn.style.opacity = confirmBtn.disabled ? '0.45' : '1';
confirmBtn.style.cursor = confirmBtn.disabled ? 'not-allowed' : 'pointer';
@@ -1103,21 +1093,13 @@ export function _openGalleryReplacer(node, w, options = {}) {
compatibilityHint.style.display = 'flex';
if (mainBaseModel && selectedBaseFamily) {
compatibilityHint.textContent = zh
? `✨ 已根据主模型 ${mainBaseModel} 自动筛选兼容 LoRA;可在上方切换为全部类型。`
: `✨ Compatible LoRAs are filtered for the main model ${mainBaseModel}. Use the filter above to show all types.`;
compatibilityHint.textContent = t('pickerCompatibleMain', { name: mainBaseModel });
} else if (preferredBaseModel && selectedBaseFamily) {
compatibilityHint.textContent = zh
? `✨ 未找到已连接主模型,已根据当前 LoRA 的 ${preferredBaseModel} 标注筛选。`
: `✨ No connected main model was identified, so the current LoRA tag ${preferredBaseModel} is used.`;
compatibilityHint.textContent = t('pickerCompatibleTag', { name: preferredBaseModel });
} else if (mainBaseModel) {
compatibilityHint.textContent = zh
? `ℹ 已识别主模型 ${mainBaseModel},但没有同类 LoRA 标注,因此暂时显示全部。`
: `ℹ Main model ${mainBaseModel} was identified, but no matching LoRA tags were found, so all models remain visible.`;
compatibilityHint.textContent = t('pickerCompatibleNoTags', { name: mainBaseModel });
} else {
compatibilityHint.textContent = zh
? 'ℹ 未识别到主模型类型,暂时显示全部 LoRA;你仍可手动按基础模型筛选。'
: 'ℹ The main model type could not be identified. All LoRAs remain visible and can be filtered manually.';
compatibilityHint.textContent = t('pickerCompatibleUnknown');
}
};
@@ -1127,7 +1109,7 @@ 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() : (zh ? '全部模型' : 'All Models');
const label = folderPath ? folderPath.split('/').pop() : t('pickerAllModels');
button.textContent = `${folderPath ? '📂' : '📦'} ${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'};`;
@@ -1153,7 +1135,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
else if (sortSelect.value === 'folder-asc') paths.sort((a, b) => collator.compare(normalizePath(a), normalizePath(b)));
else paths.sort((a, b) => collator.compare(getName(a), getName(b)));
resultCount.textContent = zh ? `${paths.length} 个结果` : `${paths.length} results`;
resultCount.textContent = t('pickerResults', { count: paths.length });
stopMedia(grid);
grid.replaceChildren();
const generation = ++renderGeneration;
@@ -1197,7 +1179,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
}
if (isCurrent) {
const badge = document.createElement('span');
badge.textContent = zh ? '当前' : 'Current';
badge.textContent = t('pickerCurrent');
badge.style.cssText = 'position:absolute;top:6px;left:6px;background:rgba(255,193,7,0.92);color:#111;padding:3px 6px;border-radius:4px;font-size:10px;font-weight:800;';
previewBox.appendChild(badge);
}
@@ -1210,7 +1192,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
if (baseModel) {
const baseBadge = document.createElement('span');
baseBadge.textContent = baseModel;
baseBadge.title = `${zh ? '基础模型' : 'Base model'}: ${baseModel}`;
baseBadge.title = `${t('pickerBaseModel')}: ${baseModel}`;
baseBadge.style.cssText = 'max-width:100%;padding:3px 6px;border-radius:5px;background:rgba(7,50,43,0.9);border:1px solid rgba(70,220,185,0.25);color:#8ce1cf;font-size:9px;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 3px 8px rgba(0,0,0,0.22);';
badgeStack.appendChild(baseBadge);
}
@@ -1220,7 +1202,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
name.title = normalizePath(path);
name.style.cssText = 'padding:9px 9px 3px;font-size:12px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700;';
const folder = document.createElement('div');
folder.textContent = getFolder(path) || (zh ? '根目录' : 'Root');
folder.textContent = getFolder(path) || t('pickerRoot');
folder.title = getFolder(path);
folder.style.cssText = 'padding:0 9px 9px;font-size:9px;color:#747b8b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
card.append(previewBox, name, folder);
@@ -1252,7 +1234,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
if (paths.length) requestAnimationFrame(renderChunk);
else {
const empty = document.createElement('div');
empty.textContent = zh ? '没有符合条件的模型。' : 'No models match the current filters.';
empty.textContent = t('pickerNoMatches');
empty.style.cssText = 'color:#777;padding:30px;text-align:center;grid-column:1/-1;';
grid.appendChild(empty);
}
@@ -1300,7 +1282,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
applying = false;
updateSelection();
console.error('[Anomalous] Failed to apply model choice:', error);
alert(zh ? `操作失败:${error.message}` : `Operation failed: ${error.message}`);
alert(t('pickerOperationFailed') + error.message);
}
};
@@ -1346,7 +1328,7 @@ export function _openGalleryReplacer(node, w, options = {}) {
}).catch(error => {
if (!modal.isConnected) return;
console.error('[Anomalous] Failed to load model previews:', error);
loadingText.textContent = zh ? '⚠️ 封面加载失败,仍可按名称选择模型。' : '⚠️ Covers failed to load; models remain selectable by name.';
loadingText.textContent = t('pickerCoversFailed');
configureBaseFilter();
renderFolders();
});
@@ -1373,7 +1355,7 @@ for (let w of node.widgets) {
if (!isHealthy) {
missingNodes++;
const nodeTitle = document.createElement('div');
nodeTitle.textContent = `${window.anomalous_browser_lang === 'zh' ? '节点' : 'Node'}: ${node.title || node.type}`;
nodeTitle.textContent = `${t('doctorNodeLabel')}: ${node.title || node.type}`;
nodeTitle.style.color = '#8AB4F8';
nodeTitle.style.fontWeight = 'bold';
nodeTitle.style.marginTop = '10px';
@@ -1386,6 +1368,12 @@ if (!isHealthy) {
}
if (missingNodes === 0) {
content.innerHTML = `<div style="color:#28a745; text-align:center; padding:20px; font-size:16px;">${window.anomalous_browser_lang === 'zh' ? '🎉 太棒了!当前工作流中所有模型均在本地就绪。' : '🎉 Awesome! All models in this workflow are healthy.'}</div>`;
const healthyMessage = document.createElement('div');
healthyMessage.style.color = '#28a745';
healthyMessage.style.textAlign = 'center';
healthyMessage.style.padding = '20px';
healthyMessage.style.fontSize = '16px';
healthyMessage.textContent = t('doctorAllHealthy');
content.appendChild(healthyMessage);
}
}
+3 -7
View File
@@ -4,14 +4,10 @@
*/
import { app } from "../../../scripts/app.js";
import { i18n } from './locales.js';
import { translate } from './locales.js';
import { escapeHtml } from './safe_dom.js';
const t = (key) => {
let lang = window.anomalous_browser_lang || 'zh';
if (lang.startsWith('en')) lang = 'en';
return (i18n[lang] && i18n[lang][key]) ? i18n[lang][key] : (i18n['zh'][key] || key);
};
const t = (key, params) => translate(key, params);
function restoreWorkspaceReturnPanel(owner) {
const state = owner.workspaceReturnState;
@@ -704,7 +700,7 @@ export function sendNotebookToCanvas() {
if (!this.currentNotebook) return;
const data = this.currentNotebook.data || {};
if (!data.mainModel) {
alert(window.anomalous_browser_lang === 'zh' ? "请先选择一个主模型。" : "Please select a Main Model first.");
alert(t('notebookSelectMain'));
return;
}