Migrate detail doctor and notebook i18n
This commit is contained in:
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user