Make Model Sources a view of the Models page
The Sources chip at the end of the model types now shows, in place of the cards, where each model is downloaded: for this workflow or all models, filtered by with / without a link, searchable. Each row's link can be typed or pasted (any site), found, opened and saved with one button; a link goes to the model's own information (the editor's download link) and, for the workflow's models, into the workflow too, as one Ctrl+Z step. The canvas note and the clipboard list stay. Model Check's "Where to download" opens the view on this workflow. The pop-up goes, with its lock-then-edit links and four ways of saving; ui_model_sources.js (815 lines) splits into the view and model_source_links.js (data and actions). Old hub CSS and locale keys go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d379581853
commit
4dd183ead4
+6
-8
@@ -318,11 +318,11 @@ covered by the `styles.css` manifest.
|
||||
`shortcut_controls.js` owns the open-browser and material-library keyboard
|
||||
shortcuts, their fallback when ComfyUI's keybinding does not fire, and the
|
||||
settings control that opens ComfyUI's keybinding editor.
|
||||
- `ui_model_sources.js` owns the Model Sources Hub, managing workflow-model and global-library source detection, Civitai/HuggingFace URL attribution, sidecar persistence, and resilient scope switching between active workflow and full local library (with cached library state preservation and reliable re-rendering).
|
||||
- `ui_model_sources.js` renders the Models page's Sources view (the last type chip): where each model is downloaded, for the open workflow or every model, with each link editable and saved in place; `model_source_links.js` owns its data and actions (collecting the workflow's models, resolving them here, saving a link to the model's information and the workflow, the canvas note and the clipboard list).
|
||||
- `ui_materials.js`, `ui_material_cards.js`, and `ui_material_application.js` own the Material Library UI, category navigation, card presentation (with grab cursor affordances, explicit drag tooltips, and polymorphic card dragging via `bindPolymorphicMaterialCardDrag`), context-aware drag guidance, prompt drops into the prompt box under the pointer (`prompt_drop.js`), and the structured empty state onboarding blueprint guiding users through collection, canvas drag, and prompt studio mixing. Drag precedence prioritizes node hits over blank canvas drops; blank canvas drops create `CLIPTextEncode` nodes (one per side the prompt has) or open full workflows. `node_material_actions.js` owns prompt envelope extraction (`extractMaterialPromptEnvelope`) shared by detail views, cards and canvas actions.
|
||||
- `ui_update_guide.js` and `update_guide_data.js` own the non-intrusive update guide modal (accessible via header button `#anomalous-update-notice-btn`, the Help modal and Home's "What's new"; the current guide's ID and steps live in `update_guide_data.js`) with full bilingual localization. `ui_spotlight_tour.js` provides the interactive spotlight mask tour (`startSpotlightTour`), gliding smooth focal box highlights across the rail's pages and tools with directional tooltip cards and keyboard navigation; steps whose target is not on screen are skipped. Other views pass their own `steps` (text from locale keys) and an optional `onClose`; the GPT-SoVITS import window does.
|
||||
- `tool_registry.js` holds the tool icons shared by the rail and Home.
|
||||
- The rail's tool slots (`ui_rail_tools.js`, fixed) hold scan, doctor and current node; settings sits at the rail's bottom. Materials is a rail page. There is no toolbox: the other tools open from their pages (workflow share codes on Workflows, Prompt Studio and Prompt Notes on Materials, Model Sources on Models and in the doctor, translation in Current node).
|
||||
- The rail's tool slots (`ui_rail_tools.js`, fixed) hold scan, doctor and current node; settings sits at the rail's bottom. Materials is a rail page. There is no toolbox: the other tools open from their pages (workflow share codes on Workflows, Prompt Studio and Prompt Notes on Materials, Model Sources as a view of Models, opened from the doctor too, translation in Current node).
|
||||
- `ui_model_types.js` owns the models page's type chips (one per models folder, with its count) and
|
||||
`owner.modelScope`, what the grid lists: a whole type, or one list folder shown as a crumb. The
|
||||
grid's cards set `currentType/PathIdx/Subfolder` to their own model's folder (`focusModel`),
|
||||
@@ -424,12 +424,10 @@ covered by the `styles.css` manifest.
|
||||
parameter sets) and works out what each would change on the node, keeping seeds,
|
||||
model files and missing choices. `model_picker.js`, `node_material_actions.js`, and `graph_splice.js`
|
||||
own the remaining explicit graph changes.
|
||||
- `ui_model_sources.js` owns the Model Source Hub (模型来源统一中控中心), providing dual-scope
|
||||
(Workflow and Library) source inspection, external platform jumping, canvas `Note` node generation,
|
||||
`workflow.extra.anomalous_model_sources` metadata synchronization, automated asynchronous model
|
||||
metadata resolution (`resolveWorkflowModelsMetadata`) via `/anomalous/resolve_paths_to_previews` with
|
||||
local sidecar priority detection, and protected read-only link display with deliberate edit-mode
|
||||
unlocking and dirty-state dynamic local persistence (hiding redundant `[Save Local]` buttons until links are modified).
|
||||
- A model's download link lives in its user layer (`source_url` in `<model>.anomalous.json`, via
|
||||
`/anomalous/update_metadata`), the same field the model editor edits; for the open workflow's
|
||||
models it is also kept in `workflow.extra.anomalous_model_sources`, so shared workflows carry it.
|
||||
Workflow models are located here through `/anomalous/resolve_paths_to_previews`.
|
||||
- `locales.js` is the shared runtime string catalog. Existing inline bilingual
|
||||
UI strings remain migration debt; new strings belong in the catalog.
|
||||
- `styles.css` is the ordered import manifest for `web/styles/*.css`, which own
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- **Current node (当前节点, formerly Node Assistant)**: the panel no longer splits into "actions" and "parameter sets" tabs; it is one page with the model actions on top and **Parameters** below. Parameters lists every saved set of values for this kind of node in one list — from the material library and from recipes' parameter sets, the same values saved twice shown once — and each entry says exactly what it would change on the selected node ("steps: 20 → 30"). **Apply these N** changes only those; seeds and model files stay (change the model above), and the receipt undoes it. **Save these values** keeps the node's current values in the material library, so the next node of that kind can use them.
|
||||
- **Translation moved onto the node (翻译挪到节点上)**: Current node now lists the selected node's prompt boxes, each marked positive or negative by its wiring. A box written in Chinese has **Translate to English**, which writes the English back (the receipt undoes it); an English box has **Show in Chinese**, which shows the meaning underneath and writes nothing. The separate Prompt Translator window is gone; Prompt Studio keeps its own translate buttons.
|
||||
- **No more toolbox (工具箱拆掉了)**: every tool now opens from the page it belongs to. Workflow share codes (import / export) are the ⇅ button at the top right of **Workflows** (where a disabled import button used to sit); **Prompt Studio** and **Prompt Notes** are at the top right of **Materials**; **Model Sources** is at the end of the type chips on **Models**, and Model Check has **Where to download** for the open workflow's models; translation is on the prompt boxes in **Current node**. The rail keeps scan, Model Check and Current node.
|
||||
- **Model sources on the Models page (模型来源进了模型页)**: the Sources chip at the end of the model types now shows, in place of the cards, where each model is downloaded, for **This workflow** or **All models**, filtered by with / without a link. Every link can be typed or pasted straight into its row (any site, not only Civitai), found automatically, opened and saved with one button; a link is kept in the model's own information (the same as the editor's download link) and, for this workflow's models, in the workflow too, so it travels when the workflow is shared. The list can go on the canvas as a note or to the clipboard. Model Check's **Where to download** opens it on This workflow. The separate pop-up, its lock-then-edit links and its four ways of saving are gone.
|
||||
- **One look everywhere (配色统一)**: pages made at different times used their own greys (bluish slate here, neutral grey there) and their own accent (sky blue, violet, indigo). They now all take the Home page's colours: neutral surfaces, the same text greys and one accent for links and selection, and they follow the Abyssal Scarlet theme too (the Material Library, Prompt Studio, voices and several dialogs used to stay blue-grey in it). The workflow share-code dialogs lost their blue and orange buttons for the usual ones. Status colours and prompt-role colours are unchanged.
|
||||
- **See it before you let go (拖的时候先看到结果)**: dragging a prompt over a prompt box shows, in that box and in the opposite box on the same sampler, the text releasing would put there; move away and they go back. Dragging saved values over a node of their kind lists what would change, such as "steps 12 → 28, cfg 4.5 → 5.5", or says nothing would change.
|
||||
- **The scan page in plain words (扫描页说人话)**: it says what a scan does (the file's fingerprint is looked up on Civitai), and why a model was not found: not published on Civitai (voice, upscale and detection models mostly come from HuggingFace or GitHub), trained or merged by you, or changed after download; it works as usual, and you can add its download link. A model Civitai does not know that is no known image model says it may be a voice or upscale model. Model files in other formats (.pth, .ckpt, .bin…), which scans never read, are listed under **Other formats** with why, so they are no longer a mystery.
|
||||
|
||||
@@ -146,14 +146,15 @@ query parameter.
|
||||
|
||||
## Model source component links
|
||||
|
||||
The source hub uses `inferModelFolderTypes` for workflow component labels and
|
||||
The Sources view (`ui_model_sources.js`, data in `model_source_links.js`) uses
|
||||
`inferModelFolderTypes` for workflow component labels and
|
||||
category-scoped, exact-path local metadata reads. Context requests are batched
|
||||
to respect the endpoint's 16-item limit. Missing source links and unavailable
|
||||
native model choices are independent states; dynamic choices are not treated
|
||||
as evidence of absence. Native extensionless component choices and PTH/GGUF
|
||||
references remain visible even when the backend cannot locate an individual file.
|
||||
|
||||
`model_source_data.js` owns library-result shaping and the source hub's pure
|
||||
`model_source_data.js` owns library-result shaping and the Sources view's pure
|
||||
main/component grouping and filtering. CLIP, text encoder, CLIP Vision, VAE, and
|
||||
preview-VAE entries live in a session-local, default-collapsed disclosure; the
|
||||
source-status pills and search affect both main models and components. Collapsed
|
||||
|
||||
@@ -19,6 +19,7 @@ import { openPromptStudio } from './ui_prompt_composer.js';
|
||||
import { closeUpdateGuide } from './ui_update_guide.js';
|
||||
import { showImageWorkbench } from './ui_gallery_detail.js';
|
||||
import { openDoctorPage } from './ui_doctor.js';
|
||||
import { showModelSources } from './ui_model_sources.js';
|
||||
import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, diagnoseNode, openCurrentNode, openLoraInsertionPicker } from './ui_node_assistant.js';
|
||||
import { _openGalleryReplacer } from './ui_node_model_picker.js';
|
||||
import { renderAudioStudio, stopAudioStudioPlayback } from './ui_audio_studio.js';
|
||||
@@ -137,6 +138,7 @@ export class AnomalousBrowser {
|
||||
}
|
||||
|
||||
AnomalousBrowser.prototype.openDoctorPage = function () { openDoctorPage(this); };
|
||||
AnomalousBrowser.prototype.showModelSources = function (scope) { showModelSources(this, scope); };
|
||||
AnomalousBrowser.prototype.diagnoseNode = diagnoseNode;
|
||||
AnomalousBrowser.prototype.openCurrentNode = function () { openCurrentNode(this); };
|
||||
AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel;
|
||||
|
||||
+80
-72
@@ -1031,49 +1031,53 @@ export const i18n = {
|
||||
promptLibraryReadOnly: "由源素材管理,不能在工坊删除;拼装台中的副本可自由编辑。",
|
||||
toolWorkflowTransferTitle: "工作流导入导出中心",
|
||||
toolPromptStudioShort: '提示词工坊',
|
||||
toolModelSourcesTitle: '模型来源与发布页中控',
|
||||
toolModelSourcesTitle: "每个模型去哪下载,链接可以改成任何网站",
|
||||
toolModelSourcesShort: '模型来源',
|
||||
modelSourcesTitle: "模型来源",
|
||||
modelSourcesLead: "每个模型从哪下载。链接存在模型自己的信息里(编辑窗口里的“下载地址”就是它),哪个网站都行;“当前工作流”里的链接还会存进工作流,分享时一起带走。",
|
||||
modelSourcesOpen: "打开",
|
||||
modelSourcesSave: "保存",
|
||||
modelSourcesSavedBoth: "已存进模型信息和这个工作流",
|
||||
modelSourcesSavedWorkflow: "已存进这个工作流(本机没有这个模型)",
|
||||
modelSourcesSaveFailed: "没存上:{error}",
|
||||
modelSourcesFound: "找到了,看一下再点保存",
|
||||
modelSourcesNotFound: "没找到,可以自己粘贴链接",
|
||||
modelSourcesStats: "已填链接 {done} / {total}",
|
||||
modelSourcesKeepAll: "把链接都存进工作流",
|
||||
modelSourcesKeptAll: "已把 {count} 个链接存进工作流",
|
||||
modelSourcesWorkflowEmpty: "画布上没有用到模型",
|
||||
modelSourcesNoteTitle: "模型来源清单",
|
||||
modelSourcesNoteFile: "文件",
|
||||
modelSourcesNoteLink: "链接",
|
||||
modelSourcesNoteCount: "{count} 个模型",
|
||||
toolPromptNotesShort: '提示词笔记',
|
||||
modelSourcesModalTitle: '🌐 模型来源统一中控中心',
|
||||
modelSourcesScopeWorkflow: '当前工作流',
|
||||
modelSourcesScopeLibrary: '全部本地模型库',
|
||||
modelSourcesFilterAll: '全部',
|
||||
modelSourcesFilterResolved: '已填写来源',
|
||||
modelSourcesFilterUnresolved: '来源待补充',
|
||||
modelSourcesSearchPlaceholder: '搜索模型名称 / 节点...',
|
||||
modelSourcesAutoFetchAll: '⚡ 一键获取全部来源',
|
||||
modelSourcesGenerateNoteNode: '📝 在画布生成来源便签',
|
||||
modelSourcesSaveToWorkflow: '💾 保存至工作流',
|
||||
modelSourcesSyncToWorkflow: '🔄 同步工作流来源',
|
||||
modelSourcesCopySummary: '📋 复制当前可见范围',
|
||||
modelSourcesCopied: '✓ 已复制模型清单到剪贴板!',
|
||||
modelSourcesSavedToWorkflow: '✓ 已将模型来源保存至工作流!',
|
||||
modelSourcesSyncedToWorkflow: '✓ 已同步更新工作流中的模型来源!',
|
||||
modelSourcesNoteCreated: '✓ 已在画布生成【模型来源便签】节点!',
|
||||
modelSourcesJump: '↗️ 直达',
|
||||
modelSourcesAutoDetect: '⚡ 识别',
|
||||
modelSourcesSaveLocal: '💾 记住到本地',
|
||||
modelSourcesSavedLocal: '✓ 已将来源链接保存至本地模型信息!',
|
||||
modelSourcesStatusMissingOnDisk: '本地未安装',
|
||||
modelSourcesStatusReady: '本地就绪',
|
||||
modelSourcesStatusUnfilled: '来源待补充',
|
||||
modelSourcesStatusUnfilledOptional: '未填写(可选)',
|
||||
modelSourcesComponentsAdvanced: '基础组件(高级) · {count}',
|
||||
modelSourcesComponentsOptionalHint: '来源可选,不计入待补',
|
||||
modelSourcesComponentsMissing: '缺少本地文件 × {count}',
|
||||
modelSourcesComponentsNoSearchMatch: '当前搜索没有匹配的基础组件',
|
||||
modelSourcesMainStats: '主要模型来源:已填写',
|
||||
modelSourcesLibraryRangeHint: '本地库范围跟随设置 → 文件夹管理',
|
||||
modelSourcesLibraryLoading: '正在盘点当前文件夹管理范围…',
|
||||
modelSourcesLibraryLoadFailed: '本地模型库加载失败,请重试。',
|
||||
modelSourcesScopeWorkflow: "当前工作流",
|
||||
modelSourcesScopeLibrary: "全部模型",
|
||||
modelSourcesFilterAll: "全部",
|
||||
modelSourcesFilterResolved: "有链接",
|
||||
modelSourcesFilterUnresolved: "没填链接",
|
||||
modelSourcesSearchPlaceholder: "搜索模型或节点",
|
||||
modelSourcesGenerateNoteNode: "写进画布便签",
|
||||
modelSourcesCopySummary: "复制清单",
|
||||
modelSourcesCopied: "已复制清单",
|
||||
modelSourcesNoteCreated: "已在画布上加了一张来源便签",
|
||||
modelSourcesAutoDetect: "自动查找",
|
||||
modelSourcesSavedLocal: "已存进模型信息",
|
||||
modelSourcesStatusMissingOnDisk: "本机没有",
|
||||
modelSourcesStatusUnfilled: "没填链接",
|
||||
modelSourcesStatusUnfilledOptional: "可以不填",
|
||||
modelSourcesComponentsAdvanced: "基础组件 {count} 个",
|
||||
modelSourcesComponentsOptionalHint: "VAE、文本编码器这类,链接可以不填",
|
||||
modelSourcesLibraryLoading: "正在读取全部模型…",
|
||||
modelSourcesLibraryLoadFailed: "全部模型没有读出来。",
|
||||
modelSourcesRetry: '重试',
|
||||
modelSourcesTypeTextEncoder: '文本编码器',
|
||||
modelSourcesTypeVae: 'VAE',
|
||||
modelSourcesTypeVaeApprox: '预览 VAE',
|
||||
modelSourcesTypeVisionEncoder: '视觉编码器',
|
||||
modelSourcesLocalTargetUnknown: '尚未确定本地文件位置,可先将链接保存到工作流。',
|
||||
modelSourcesNoModelsFound: '未检测到任何模型',
|
||||
modelSourcesUrlPlaceholder: '输入或粘贴来源链接 (Civitai / Hugging Face / Liblib / 网盘 等)...',
|
||||
modelSourcesLocalTargetUnknown: "本机找不到这个模型的文件,链接没地方存。",
|
||||
modelSourcesNoModelsFound: "没有符合的模型",
|
||||
modelSourcesUrlPlaceholder: "粘贴下载页链接,哪个网站都行",
|
||||
sidebarInjectHash: '🟢 注入工作流哈希',
|
||||
sidebarSkipHash: '⚪ 不注入工作流哈希',
|
||||
sidebarManageFolders: '文件夹管理',
|
||||
@@ -2915,49 +2919,53 @@ export const i18n = {
|
||||
promptLibraryReadOnly: "Managed by the source material; cannot be deleted here. Mixer copies remain editable.",
|
||||
toolWorkflowTransferTitle: "Workflow Import / Export Center",
|
||||
toolPromptStudioShort: 'Prompt Studio',
|
||||
toolModelSourcesTitle: 'Model Source Hub',
|
||||
toolModelSourcesTitle: "Where each model is downloaded; any site's link can go here",
|
||||
toolModelSourcesShort: 'Sources',
|
||||
modelSourcesTitle: "Model sources",
|
||||
modelSourcesLead: "Where each model is downloaded. A link is kept in the model's own information (the editor's download link is the same), from any site; links of this workflow's models are kept in the workflow too, so they travel with it.",
|
||||
modelSourcesOpen: "Open",
|
||||
modelSourcesSave: "Save",
|
||||
modelSourcesSavedBoth: "Saved in the model's information and this workflow",
|
||||
modelSourcesSavedWorkflow: "Saved in this workflow (the model is not on this computer)",
|
||||
modelSourcesSaveFailed: "Not saved: {error}",
|
||||
modelSourcesFound: "Found one; check it, then Save",
|
||||
modelSourcesNotFound: "Nothing found; paste the link yourself",
|
||||
modelSourcesStats: "With a link: {done} / {total}",
|
||||
modelSourcesKeepAll: "Keep all links in the workflow",
|
||||
modelSourcesKeptAll: "Kept {count} links in the workflow",
|
||||
modelSourcesWorkflowEmpty: "The canvas uses no models",
|
||||
modelSourcesNoteTitle: "Model sources",
|
||||
modelSourcesNoteFile: "File",
|
||||
modelSourcesNoteLink: "Link",
|
||||
modelSourcesNoteCount: "{count} models",
|
||||
toolPromptNotesShort: 'Prompt Notes',
|
||||
modelSourcesModalTitle: '🌐 Model Source Hub',
|
||||
modelSourcesScopeWorkflow: 'Active Workflow',
|
||||
modelSourcesScopeLibrary: 'All Local Models',
|
||||
modelSourcesFilterAll: 'All',
|
||||
modelSourcesFilterResolved: 'Source provided',
|
||||
modelSourcesFilterUnresolved: 'Source needed',
|
||||
modelSourcesSearchPlaceholder: 'Search model name or node...',
|
||||
modelSourcesAutoFetchAll: '⚡ Auto-Fetch All',
|
||||
modelSourcesGenerateNoteNode: '📝 Create Note on Canvas',
|
||||
modelSourcesSaveToWorkflow: '💾 Save to Workflow',
|
||||
modelSourcesSyncToWorkflow: '🔄 Sync to Workflow',
|
||||
modelSourcesCopySummary: '📋 Copy Visible Range',
|
||||
modelSourcesCopied: '✓ Model sources copied to clipboard!',
|
||||
modelSourcesSavedToWorkflow: '✓ Model sources saved to workflow!',
|
||||
modelSourcesSyncedToWorkflow: '✓ Model sources synced in workflow!',
|
||||
modelSourcesNoteCreated: '✓ Created Model Sources Note node on canvas!',
|
||||
modelSourcesJump: '↗️ Open',
|
||||
modelSourcesAutoDetect: '⚡ Detect',
|
||||
modelSourcesSaveLocal: '💾 Save Local',
|
||||
modelSourcesSavedLocal: '✓ Saved source link to local model metadata!',
|
||||
modelSourcesStatusMissingOnDisk: 'Not installed locally',
|
||||
modelSourcesStatusReady: 'Installed',
|
||||
modelSourcesStatusUnfilled: 'Source needed',
|
||||
modelSourcesStatusUnfilledOptional: 'Not provided (optional)',
|
||||
modelSourcesComponentsAdvanced: 'Foundation components (advanced) · {count}',
|
||||
modelSourcesComponentsOptionalHint: 'Sources are optional and excluded from source-needed counts',
|
||||
modelSourcesComponentsMissing: 'Missing local files × {count}',
|
||||
modelSourcesComponentsNoSearchMatch: 'No foundation components match this search',
|
||||
modelSourcesMainStats: 'Main model sources filled',
|
||||
modelSourcesLibraryRangeHint: 'Local library scope follows Settings → Folder Manager',
|
||||
modelSourcesLibraryLoading: 'Loading the current Folder Manager scope…',
|
||||
modelSourcesLibraryLoadFailed: 'Failed to load the local model library. Please retry.',
|
||||
modelSourcesScopeWorkflow: "This workflow",
|
||||
modelSourcesScopeLibrary: "All models",
|
||||
modelSourcesFilterAll: "All",
|
||||
modelSourcesFilterResolved: "With a link",
|
||||
modelSourcesFilterUnresolved: "No link yet",
|
||||
modelSourcesSearchPlaceholder: "Search models or nodes",
|
||||
modelSourcesGenerateNoteNode: "Add a note to the canvas",
|
||||
modelSourcesCopySummary: "Copy the list",
|
||||
modelSourcesCopied: "List copied",
|
||||
modelSourcesNoteCreated: "Added a sources note to the canvas",
|
||||
modelSourcesAutoDetect: "Find it",
|
||||
modelSourcesSavedLocal: "Saved in the model's information",
|
||||
modelSourcesStatusMissingOnDisk: "Not on this computer",
|
||||
modelSourcesStatusUnfilled: "no link yet",
|
||||
modelSourcesStatusUnfilledOptional: "optional",
|
||||
modelSourcesComponentsAdvanced: "{count} components",
|
||||
modelSourcesComponentsOptionalHint: "VAE, text encoders and the like; a link is optional",
|
||||
modelSourcesLibraryLoading: "Loading all models…",
|
||||
modelSourcesLibraryLoadFailed: "All models could not be loaded.",
|
||||
modelSourcesRetry: 'Retry',
|
||||
modelSourcesTypeTextEncoder: 'Text encoder',
|
||||
modelSourcesTypeVae: 'VAE',
|
||||
modelSourcesTypeVaeApprox: 'Preview VAE',
|
||||
modelSourcesTypeVisionEncoder: 'Vision encoder',
|
||||
modelSourcesLocalTargetUnknown: 'Local file location is not resolved. You can save the link to the workflow.',
|
||||
modelSourcesNoModelsFound: 'No models detected',
|
||||
modelSourcesUrlPlaceholder: 'Enter or paste source URL (Civitai / Hugging Face / Liblib / etc)...',
|
||||
modelSourcesLocalTargetUnknown: "This model's file is not on this computer, so the link has nowhere to go.",
|
||||
modelSourcesNoModelsFound: "No models match",
|
||||
modelSourcesUrlPlaceholder: "Paste the download page's link, any site",
|
||||
sidebarInjectHash: '🟢 Inject Workflow Hash',
|
||||
sidebarSkipHash: '⚪ Skip Workflow Hash',
|
||||
sidebarManageFolders: 'Manage Folders',
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Where each model can be downloaded: the data and actions behind the Models page's
|
||||
* "Sources" view (ui_model_sources.js renders it). The models of the open workflow come
|
||||
* from its loader widgets; the library's from /anomalous/all_scan_models. A link is kept
|
||||
* in the model's own information (the editor's download link) and, for a model of the
|
||||
* open workflow, in the workflow too (extra.anomalous_model_sources), so a shared
|
||||
* workflow carries it.
|
||||
*/
|
||||
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { translate as t } from './locales.js';
|
||||
import { jsonResponse } from './ui_dom.js';
|
||||
import { recordCanvasStep } from './canvas_history.js';
|
||||
import { inferModelFolderTypes, isPhysicalRenameProtectedType } from './model_policies.js';
|
||||
import { foundationModelType, shapeLibrarySourceModels, usableSourceUrl } from './model_source_data.js';
|
||||
|
||||
// A link's site, for its badge (the site's own colour).
|
||||
const PLATFORMS = [
|
||||
[/civitai\.(com|red)/i, 'Civitai', '56, 189, 248'],
|
||||
[/huggingface\.co/i, 'HuggingFace', '251, 191, 36'],
|
||||
[/liblib/i, 'LiblibAI', '236, 72, 153'],
|
||||
[/modelscope\.cn/i, 'ModelScope', '168, 85, 247'],
|
||||
[/github\.com/i, 'GitHub', '52, 211, 153'],
|
||||
[/pan\.baidu|123pan|quark|lanzou/i, 'CloudNet', '6, 182, 212'],
|
||||
];
|
||||
|
||||
export function detectPlatform(url) {
|
||||
const clean = typeof url === 'string' ? url.trim() : '';
|
||||
if (!clean) return null;
|
||||
const [, name, rgb] = PLATFORMS.find(([pattern]) => pattern.test(clean)) || [null, 'Web Link', ''];
|
||||
return rgb
|
||||
? { name, color: `rgb(${rgb})`, bg: `rgba(${rgb}, 0.15)`, border: `rgba(${rgb}, 0.4)` }
|
||||
: { name, color: 'var(--amb-text-muted)', bg: 'transparent', border: 'var(--amb-border-strong)' };
|
||||
}
|
||||
|
||||
export function isModelFilename(val) {
|
||||
return typeof val === 'string' && /\.(safetensors|ckpt|pt|pth|bin|sft|gguf)$/i.test(val);
|
||||
}
|
||||
|
||||
export function normalizeUrl(url) {
|
||||
const trimmed = typeof url === 'string' ? url.trim() : '';
|
||||
if (!trimmed) return '';
|
||||
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
||||
}
|
||||
|
||||
/** Whether the model's file is known here, so its own information can keep the link. */
|
||||
export function canSaveLocalSource(model) {
|
||||
return !model.isMissing && Boolean(model.type) && (!foundationModelType(model) || model.nodeId == null || Boolean(model.file_path));
|
||||
}
|
||||
|
||||
/** The models the open workflow's loader widgets name, one per node and file. */
|
||||
export function collectWorkflowModels() {
|
||||
const models = [];
|
||||
const seen = new Set();
|
||||
const savedSources = app.graph?.extra?.anomalous_model_sources || {};
|
||||
const savedHashes = app.graph?.extra?.anomalous_hashes || {};
|
||||
|
||||
for (const node of app.graph?._nodes || []) {
|
||||
if (!Array.isArray(node.widgets)) continue;
|
||||
for (const w of node.widgets) {
|
||||
const val = w.value;
|
||||
const folderTypes = inferModelFolderTypes(node, w);
|
||||
// Native component options such as TAESD need not have a file extension.
|
||||
const componentOption = /^(vae_name|clip_name\d*|clip_vision(?:_name)?|text_encoder(?:_name)?\d*)$/i.test(w.name || '')
|
||||
&& folderTypes.some(isPhysicalRenameProtectedType)
|
||||
&& typeof val === 'string' && val.trim() && val.toLowerCase() !== 'none';
|
||||
if (!isModelFilename(val) && !componentOption) continue;
|
||||
|
||||
const key = `${node.id}_${val}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
const basename = val.split(/[/\\]/).pop();
|
||||
const nativeValues = w.options?.values;
|
||||
const isMissing = Array.isArray(nativeValues)
|
||||
&& !nativeValues.some(value => typeof value === 'string' && value.replaceAll('\\', '/') === val.replaceAll('\\', '/'));
|
||||
const hashObj = savedHashes[key] || savedHashes[val] || window.anomalous_hash_cache?.[val] || window.anomalous_hash_cache?.[basename] || {};
|
||||
const saved = savedSources[val] || savedSources[basename] || savedSources[key];
|
||||
const url = usableSourceUrl(typeof saved === 'string' ? saved : saved?.url)
|
||||
|| usableSourceUrl(hashObj?.url) || usableSourceUrl(hashObj?.civitai_url);
|
||||
|
||||
models.push({
|
||||
key, nodeId: node.id, nodeTitle: node.title || node.type || `#${node.id}`, nodeType: node.type,
|
||||
folderTypes, filename: val, basename, isMissing,
|
||||
hash: typeof hashObj === 'string' ? hashObj : (hashObj.hash || ''),
|
||||
url, initialUrl: url, platform: detectPlatform(url),
|
||||
});
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
/** Every model in the folders the browser shows. */
|
||||
export async function fetchAllLibraryModels(signal = null) {
|
||||
const res = await fetch('/anomalous/all_scan_models?limit=0', { signal });
|
||||
const payload = await jsonResponse(res, 'load library models');
|
||||
return shapeLibrarySourceModels(payload.models, detectPlatform);
|
||||
}
|
||||
|
||||
/** Fills in where the workflow's models live here (type, folder) and links their information has. */
|
||||
export async function resolveWorkflowModelsMetadata(models, signal = null) {
|
||||
if (!Array.isArray(models) || !models.length) return false;
|
||||
const components = models.filter(m => foundationModelType(m));
|
||||
const requestedPaths = models.filter(m => !foundationModelType(m)).map(m => m.filename);
|
||||
try {
|
||||
const res = await fetch('/anomalous/resolve_paths_to_previews', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: requestedPaths }), signal,
|
||||
});
|
||||
const modelsMap = (await jsonResponse(res, 'resolve workflow models'))?.models || {};
|
||||
const componentModels = {};
|
||||
// The endpoint caps contextual lookups at 16; keep large workflows complete.
|
||||
for (let offset = 0; offset < components.length; offset += 16) {
|
||||
const response = await fetch('/anomalous/resolve_paths_to_previews', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, signal,
|
||||
body: JSON.stringify({ paths: [], context_requests: components.slice(offset, offset + 16).map(m => ({
|
||||
key: m.key, path: m.relPath || m.filename, folder_types: m.folderTypes || [m.type], exact_only: true,
|
||||
})) }),
|
||||
});
|
||||
Object.assign(componentModels, (await jsonResponse(response, 'resolve component sources')).context_models || {});
|
||||
}
|
||||
let changed = false;
|
||||
for (const m of models) {
|
||||
const info = foundationModelType(m) ? componentModels[m.key] : modelsMap[m.filename] || modelsMap[m.basename];
|
||||
if (!info) continue;
|
||||
Object.assign(m, { type: info.type, subfolder: info.subfolder, path_idx: info.path_idx, file_path: info.file_path });
|
||||
if (foundationModelType(m)) m.isMissing = false;
|
||||
if (!m.hash && info.metadata?.hash) m.hash = info.metadata.hash;
|
||||
const resolvedUrl = usableSourceUrl(info.metadata?.source_url) || usableSourceUrl(info.metadata?.civitai_url);
|
||||
if (!m.url && resolvedUrl) Object.assign(m, { url: resolvedUrl, initialUrl: resolvedUrl, platform: detectPlatform(resolvedUrl) });
|
||||
if (window.anomalous_hash_cache && m.filename && info.metadata?.hash) {
|
||||
window.anomalous_hash_cache[m.filename] = { hash: info.metadata.hash, url: resolvedUrl };
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) console.warn('[AMB] Model sources: could not resolve the workflow\'s models.', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes `url` into the open workflow's sources for `model` ('' removes it); one Ctrl+Z step. */
|
||||
function keepInWorkflow(model, url) {
|
||||
if (!app.graph) return false;
|
||||
app.graph.extra ||= {};
|
||||
const sources = { ...(app.graph.extra.anomalous_model_sources || {}) };
|
||||
if (url) {
|
||||
sources[model.filename] = { name: model.filename, url, platform: detectPlatform(url)?.name || 'Custom', nodeId: model.nodeId, hash: model.hash || '', updated_at: Date.now() };
|
||||
} else {
|
||||
delete sources[model.filename];
|
||||
}
|
||||
app.graph.extra.anomalous_model_sources = sources;
|
||||
app.canvas?.setDirty(true, true);
|
||||
recordCanvasStep(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a model's link: in its own information when the file is known here, and in the
|
||||
* open workflow for one of its models. Returns where: { local, workflow }.
|
||||
*/
|
||||
export async function saveModelSource(model, rawUrl) {
|
||||
const url = normalizeUrl(rawUrl);
|
||||
const where = { local: false, workflow: false };
|
||||
if (canSaveLocalSource(model)) {
|
||||
const res = await fetch('/anomalous/update_metadata', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
filename: model.basename || model.filename.split(/[/\\]/).pop(),
|
||||
type: model.type, subfolder: model.subfolder ? `/${String(model.subfolder).replace(/^\/+/, '')}` : '/',
|
||||
path_idx: model.path_idx || 0, custom_source_url: url,
|
||||
}),
|
||||
});
|
||||
const payload = await jsonResponse(res, 'save model source');
|
||||
if (payload.status !== 'success') throw new Error(payload.message || 'save model source failed');
|
||||
where.local = true;
|
||||
}
|
||||
if (model.nodeId != null) where.workflow = keepInWorkflow(model, url);
|
||||
if (!where.local && !where.workflow) throw new Error(t('modelSourcesLocalTargetUnknown'));
|
||||
Object.assign(model, { url, initialUrl: url, platform: detectPlatform(url) });
|
||||
return where;
|
||||
}
|
||||
|
||||
/** Puts every link the listed workflow models have into the workflow; how many. */
|
||||
export function keepAllInWorkflow(models) {
|
||||
if (!app.graph) return 0;
|
||||
app.graph.extra ||= {};
|
||||
const sources = { ...(app.graph.extra.anomalous_model_sources || {}) };
|
||||
let count = 0;
|
||||
for (const m of models) {
|
||||
if (!m.url) continue;
|
||||
const url = normalizeUrl(m.url);
|
||||
sources[m.filename] = { name: m.filename, url, platform: detectPlatform(url)?.name || 'Custom', nodeId: m.nodeId, hash: m.hash || '', updated_at: Date.now() };
|
||||
count++;
|
||||
}
|
||||
app.graph.extra.anomalous_model_sources = sources;
|
||||
app.canvas?.setDirty(true, true);
|
||||
recordCanvasStep(app);
|
||||
return count;
|
||||
}
|
||||
|
||||
const timestamp = () => {
|
||||
const now = new Date();
|
||||
const two = n => String(n).padStart(2, '0');
|
||||
return `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())} ${two(now.getHours())}:${two(now.getMinutes())}`;
|
||||
};
|
||||
|
||||
/** The list as lines: one model and its link each. */
|
||||
function sourceLines(models, link) {
|
||||
return models.map((m, idx) => {
|
||||
const source = m.url || (foundationModelType(m) ? t('modelSourcesStatusUnfilledOptional') : t('modelSourcesStatusUnfilled'));
|
||||
return link(m, idx, source);
|
||||
});
|
||||
}
|
||||
|
||||
/** A Note node on the canvas listing the workflow's models and links, above the others. */
|
||||
export function createCanvasNoteNode(models) {
|
||||
const creator = globalThis.LiteGraph?.createNode;
|
||||
const noteNode = models.length && typeof creator === 'function' ? creator.call(globalThis.LiteGraph, 'Note') : null;
|
||||
if (!noteNode) return null;
|
||||
const lines = sourceLines(models, (m, idx, source) => `[${idx + 1}] ${m.nodeTitle || m.nodeType || 'Model'}\n ${t('modelSourcesNoteFile')}: ${m.filename}\n ${t('modelSourcesNoteLink')}: ${source}`);
|
||||
const text = `${t('modelSourcesNoteTitle')}\n${timestamp()} · ${t('modelSourcesNoteCount', { count: models.length })}\n\n${lines.join('\n\n')}`;
|
||||
noteNode.title = t('modelSourcesNoteTitle');
|
||||
if (noteNode.widgets?.length) noteNode.widgets[0].value = text;
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
for (const n of app.graph._nodes || []) {
|
||||
if (!n.pos) continue;
|
||||
minX = Math.min(minX, n.pos[0]);
|
||||
minY = Math.min(minY, n.pos[1]);
|
||||
}
|
||||
if (!Number.isFinite(minX)) { minX = 100; minY = 100; }
|
||||
noteNode.pos = [minX, minY - 320];
|
||||
noteNode.size = [480, 260];
|
||||
app.graph.add(noteNode);
|
||||
app.canvas?.setDirty(true, true);
|
||||
recordCanvasStep(app);
|
||||
return noteNode;
|
||||
}
|
||||
|
||||
/** The list as Markdown on the clipboard. */
|
||||
export async function copySourcesSummary(models) {
|
||||
const lines = sourceLines(models, (m, idx, source) => `${idx + 1}. **${m.nodeTitle || m.type || ''}** \`${m.basename || m.filename}\` — ${source}`);
|
||||
await navigator.clipboard.writeText(`### ${t('modelSourcesNoteTitle')}\n\n${lines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
export function openExternalUrl(rawUrl) {
|
||||
const normalized = normalizeUrl(rawUrl);
|
||||
if (normalized) window.open(normalized, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
/** A link for a model without one: its local information, else Civitai by hash; '' when none. */
|
||||
export async function autoDetectModelSource(item) {
|
||||
if (foundationModelType(item)) {
|
||||
await resolveWorkflowModelsMetadata([item]);
|
||||
return item.url || '';
|
||||
}
|
||||
try {
|
||||
const queryPath = item.filename || item.basename;
|
||||
const res = await fetch('/anomalous/resolve_paths_to_previews', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: [queryPath, item.basename].filter(Boolean) }),
|
||||
});
|
||||
const data = await jsonResponse(res, 'detect model source local');
|
||||
const resolved = data?.models?.[queryPath] || data?.models?.[item.basename];
|
||||
if (resolved) {
|
||||
Object.assign(item, { type: resolved.type, subfolder: resolved.subfolder, path_idx: resolved.path_idx });
|
||||
if (resolved.metadata?.hash) item.hash = resolved.metadata.hash;
|
||||
const found = usableSourceUrl(resolved.metadata?.source_url) || usableSourceUrl(resolved.metadata?.civitai_url);
|
||||
if (found) return found;
|
||||
}
|
||||
} catch (e) {
|
||||
// the local lookup failed; Civitai by hash may still know it
|
||||
}
|
||||
if (item.hash) {
|
||||
try {
|
||||
const res = await fetch(`https://civitai.com/api/v1/model-versions/by-hash/${item.hash}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.modelId) {
|
||||
const domain = (data.model?.nsfw || data.nsfwLevel > 1) ? 'civitai.red' : 'civitai.com';
|
||||
return `https://${domain}/models/${data.modelId}${data.id ? `?modelVersionId=${data.id}` : ''}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// no connection: nothing found
|
||||
}
|
||||
}
|
||||
// A search-results page is not a model source.
|
||||
return '';
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
import { translate as t } from './locales.js';
|
||||
import { applyModelFix, checkWorkflowModels, fixWorkflowModels, isProblem, markReplaced, replacedFrom } from './model_check.js';
|
||||
import { updateDoctorBanner } from './ui_doctor_banner.js';
|
||||
import { openModelSourcesModal } from './ui_model_sources.js';
|
||||
|
||||
const MODEL_EXT = /\.(safetensors|ckpt|pt|bin|pth|sft)$/i;
|
||||
const ORDER = ['fixable', 'candidate', 'conflict', 'ambiguous', 'missing', 'changed'];
|
||||
@@ -192,7 +191,7 @@ export async function renderDoctorPage(owner, panel, { refresh = false } = {}) {
|
||||
}));
|
||||
}
|
||||
actions.append(button('anomalous-scan-secondary', t('doctorRecheck'), () => renderDoctorPage(owner, panel, { refresh: true })),
|
||||
button('anomalous-scan-secondary', t('doctorSources'), () => openModelSourcesModal('workflow')));
|
||||
button('anomalous-scan-secondary', t('doctorSources'), () => owner.showModelSources('workflow')));
|
||||
page.append(actions);
|
||||
const verdict = !entries.length ? t('doctorNoModels')
|
||||
: !entries.some(isProblem) ? t('doctorAllHere', { count: entries.length })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { app } from "../../../scripts/app.js";
|
||||
import { translate } from './locales.js';
|
||||
import { focusModel, modelListUrl, renderTypeBar } from './ui_model_types.js';
|
||||
import { renderSourceBadge } from './model_source.js';
|
||||
import { renderModelSourcesView } from './ui_model_sources.js';
|
||||
import { recordCanvasStep } from './canvas_history.js';
|
||||
|
||||
const t = (key, params) => translate(key, params);
|
||||
@@ -26,6 +27,13 @@ function modelCardDisplayName(model) {
|
||||
export async function loadModels() {
|
||||
try {
|
||||
if (this._modelLoadController) this._modelLoadController.abort();
|
||||
this.modelSources?.controller?.abort();
|
||||
if (this.modelView === 'sources') { // the type bar, then where each model is downloaded
|
||||
stopMediaInContainer(this.grid);
|
||||
this.grid.replaceChildren(renderTypeBar(this, 0));
|
||||
renderModelSourcesView(this, this.grid);
|
||||
return;
|
||||
}
|
||||
this._modelRenderGeneration = (this._modelRenderGeneration || 0) + 1;
|
||||
const loadController = new AbortController();
|
||||
this._modelLoadController = loadController;
|
||||
|
||||
+218
-783
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,11 @@
|
||||
* `owner.modelScope` is what the grid lists: `{type, path_idx}` for a chip, plus
|
||||
* `subfolder` for a list folder. `currentType/PathIdx/Subfolder` stay "where the model at
|
||||
* hand lives", which the editor, the scanner and "add to canvas" read (see focusModel).
|
||||
* The last chip, Sources, shows where each model is downloaded instead of the cards
|
||||
* (`owner.modelView === 'sources'`, ui_model_sources.js); a type chip goes back.
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { openModelSourcesModal } from './ui_model_sources.js';
|
||||
|
||||
const SCOPE_KEY = 'anomalous_model_scope';
|
||||
|
||||
@@ -49,6 +50,7 @@ function showType(owner, group) {
|
||||
}
|
||||
|
||||
function openType(owner, group) {
|
||||
owner.modelView = '';
|
||||
showType(owner, group);
|
||||
owner.renderSidebar();
|
||||
owner.loadModels();
|
||||
@@ -94,8 +96,9 @@ export function renderTypeBar(owner, listed) {
|
||||
bar.setAttribute('aria-label', t('modelTypesLabel'));
|
||||
const scope = owner.modelScope;
|
||||
const groups = owner.foldersData || [];
|
||||
const inSources = owner.modelView === 'sources';
|
||||
for (const group of groups) {
|
||||
const active = sameGroup(group, scope);
|
||||
const active = !inSources && sameGroup(group, scope);
|
||||
const whole = active && !scope.subfolder;
|
||||
// The folder counts date from the last folder load; the listed type counts what it shows.
|
||||
const count = whole ? listed : modelCount(group);
|
||||
@@ -114,7 +117,7 @@ export function renderTypeBar(owner, listed) {
|
||||
chip.onclick = () => { if (!whole) openType(owner, group); };
|
||||
bar.appendChild(chip);
|
||||
}
|
||||
const group = scope?.subfolder && groups.find(item => sameGroup(item, scope));
|
||||
const group = !inSources && scope?.subfolder && groups.find(item => sameGroup(item, scope));
|
||||
if (group) {
|
||||
const crumb = document.createElement('button');
|
||||
crumb.type = 'button';
|
||||
@@ -131,13 +134,15 @@ export function renderTypeBar(owner, listed) {
|
||||
crumb.onclick = () => openType(owner, group);
|
||||
bar.appendChild(crumb);
|
||||
}
|
||||
// Where each model can be downloaded (the Model Source Hub, library scope).
|
||||
// Where each model can be downloaded, in place of the cards.
|
||||
const sources = document.createElement('button');
|
||||
sources.type = 'button';
|
||||
sources.className = 'anomalous-model-type-chip is-tool';
|
||||
sources.classList.toggle('is-active', inSources);
|
||||
sources.setAttribute('aria-pressed', String(inSources));
|
||||
sources.textContent = t('toolModelSourcesShort');
|
||||
sources.title = t('toolModelSourcesTitle');
|
||||
sources.onclick = () => openModelSourcesModal('library');
|
||||
sources.onclick = () => { if (!inSources) owner.showModelSources('library'); };
|
||||
bar.appendChild(sources);
|
||||
return bar;
|
||||
}
|
||||
|
||||
@@ -402,6 +402,7 @@ export function renderSidebar() {
|
||||
return;
|
||||
}
|
||||
this.modelScope = { type: typeGroup.type, path_idx: typeGroup.path_idx, subfolder: path };
|
||||
this.modelView = '';
|
||||
this.currentType = typeGroup.type;
|
||||
this.currentPathIdx = typeGroup.path_idx;
|
||||
this.currentSubfolder = path;
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
@import url("./styles/07-prompt-studio.css?v=20261002-theme-1");
|
||||
@import url("./styles/08-theme-gallery-overrides.css?v=20261002-theme-1");
|
||||
@import url("./styles/09-prompt-drawers.css?v=20261002-theme-1");
|
||||
@import url("./styles/10-model-sources.css?v=20261002-theme-1");
|
||||
@import url("./styles/10-model-sources.css?v=20261002-sources-1");
|
||||
@import url("./styles/11-audio-studio.css?v=20261002-theme-1");
|
||||
@import url("./styles/12-tts-setup.css?v=20261002-theme-1");
|
||||
@import url("./styles/13-script-director.css?v=20261002-theme-1");
|
||||
|
||||
+213
-379
@@ -1,429 +1,263 @@
|
||||
/* Model Source Hub Modal (模型来源统一中控中心) */
|
||||
/* ========================================================================= */
|
||||
.anomalous-model-sources-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 7, 12, 0.78);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
z-index: 1000008;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
animation: anmFadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.anomalous-model-sources-modal {
|
||||
width: 880px;
|
||||
max-width: 96vw;
|
||||
max-height: 88vh;
|
||||
background: var(--amb-bg-page);
|
||||
border: 1px solid rgba(244, 63, 94, 0.35);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.85), 0 0 32px rgba(190, 20, 55, 0.18);
|
||||
/* Model Sources: the Models page's view of where each model is downloaded (ui_model_sources.js). */
|
||||
.anomalous-msrc {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
padding: 4px 2px 12px;
|
||||
container-type: inline-size;
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sources-header {
|
||||
padding: 14px 18px 0;
|
||||
border-bottom: 1px solid var(--amb-border);
|
||||
background: rgba(18, 20, 28, 0.9);
|
||||
flex-shrink: 0;
|
||||
.anomalous-msrc-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sources-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.anomalous-sources-title-wrap h3 {
|
||||
.anomalous-msrc-lead {
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sources-close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.anomalous-sources-close-btn:hover {
|
||||
color: var(--amb-text-main);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.anomalous-sources-scope-tabs {
|
||||
.anomalous-msrc-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-sources-scope-tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--amb-text-muted);
|
||||
padding: 8px 16px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-sources-scope-tab:hover {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sources-scope-tab.is-active {
|
||||
color: var(--amb-link);
|
||||
border-bottom-color: var(--amb-link);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sources-badge-num {
|
||||
font-size: 0.72rem;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--amb-text-soft);
|
||||
}
|
||||
|
||||
.anomalous-sources-filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid var(--amb-border);
|
||||
background: rgba(14, 16, 23, 0.95);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anomalous-sources-pills {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-sources-pill {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-muted);
|
||||
border-radius: 16px;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-sources-pill:hover {
|
||||
color: var(--amb-text-main);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.anomalous-sources-pill.is-active {
|
||||
background: color-mix(in srgb, var(--amb-link) 18%, transparent);
|
||||
border-color: color-mix(in srgb, var(--amb-link) 40%, transparent);
|
||||
color: var(--amb-link);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-sources-search-input {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 6px;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 0.8rem;
|
||||
padding: 5px 10px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-sources-search-input:focus {
|
||||
border-color: var(--amb-link);
|
||||
}
|
||||
|
||||
.anomalous-sources-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.anomalous-source-row-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-source-row-item.has-url {
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.anomalous-source-row-item.is-missing-url {
|
||||
border-left: 3px solid #64748b;
|
||||
}
|
||||
|
||||
.anomalous-source-row-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anomalous-source-node-tag {
|
||||
font-size: 0.72rem;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(244, 63, 94, 0.15);
|
||||
border: 1px solid rgba(244, 63, 94, 0.3);
|
||||
color: #fb7185;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.anomalous-source-model-name {
|
||||
font-size: 0.88rem;
|
||||
color: var(--amb-text-main);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.anomalous-source-badge-neutral {
|
||||
font-size: 0.72rem;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
color: var(--amb-text-soft);
|
||||
}
|
||||
|
||||
.anomalous-source-badge-danger {
|
||||
font-size: 0.7rem;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.anomalous-source-hash-chip {
|
||||
font-size: 0.72rem;
|
||||
color: var(--amb-text-dim);
|
||||
margin-left: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.anomalous-source-col-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.anomalous-source-input-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.anomalous-source-url-input {
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
.anomalous-msrc-pills {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 6px;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 0.82rem;
|
||||
padding: 6px 10px;
|
||||
padding-right: 96px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
font-family: monospace;
|
||||
border-radius: 999px;
|
||||
background: var(--amb-bg-input);
|
||||
}
|
||||
|
||||
.anomalous-source-url-input:focus {
|
||||
border-color: var(--amb-link);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.anomalous-source-url-input.is-locked {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-color: var(--amb-border);
|
||||
.anomalous-msrc-pill {
|
||||
padding: 3px 11px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--amb-text-muted);
|
||||
cursor: default;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-source-url-input.is-locked:hover {
|
||||
border-color: var(--amb-border-strong);
|
||||
}
|
||||
|
||||
.anomalous-source-url-input.is-editing {
|
||||
border-color: #f59e0b;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.25);
|
||||
.anomalous-msrc-pill:hover {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-source-edit-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
color: #fbbf24;
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
.anomalous-msrc-pill.is-active {
|
||||
background: color-mix(in srgb, var(--amb-link) 16%, transparent);
|
||||
color: var(--amb-link);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-msrc-search {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 999px;
|
||||
background: var(--amb-bg-input);
|
||||
color: var(--amb-text-main);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.anomalous-msrc-search:focus {
|
||||
border-color: color-mix(in srgb, var(--amb-link) 45%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-msrc-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-msrc-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 1fr) minmax(260px, 1.6fr);
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: var(--amb-radius-card);
|
||||
background: var(--amb-bg-card);
|
||||
}
|
||||
|
||||
.anomalous-msrc-row.is-dirty {
|
||||
border-color: color-mix(in srgb, var(--amb-link) 45%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-msrc-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anomalous-msrc-name {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
font-size: 0.88rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-btn-save-local {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
|
||||
border-color: rgba(245, 158, 11, 0.6) !important;
|
||||
color: var(--amb-text-main) !important;
|
||||
font-weight: 600;
|
||||
animation: anomalousSourcesPulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes anomalousSourcesPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); }
|
||||
50% { box-shadow: 0 0 0 4px rgba(245, 158, 11, 0); }
|
||||
}
|
||||
|
||||
.anomalous-source-plat-badge {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.anomalous-source-row-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anomalous-btn-jump {
|
||||
background: linear-gradient(135deg, var(--amb-link), #0369a1);
|
||||
border: 1px solid color-mix(in srgb, var(--amb-link) 40%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-btn-jump:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.anomalous-sources-footer {
|
||||
padding: 10px 18px;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
background: rgba(18, 20, 28, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anomalous-sources-footer-stats {
|
||||
font-size: 0.82rem;
|
||||
color: var(--amb-text-muted);
|
||||
}
|
||||
|
||||
.anomalous-sources-footer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-sources-empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.anomalous-sources-library-hint {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--amb-text-secondary, var(--amb-text-muted));
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 7px;
|
||||
.anomalous-msrc-where {
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.anomalous-sources-components-toggle {
|
||||
width: 100%;
|
||||
.anomalous-msrc-tag {
|
||||
padding: 1px 7px;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.anomalous-msrc-tag.is-missing {
|
||||
border-color: rgba(248, 113, 113, 0.45);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.anomalous-msrc-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
padding: 11px 12px;
|
||||
color: var(--amb-text-secondary, var(--amb-text-soft));
|
||||
background: rgba(148, 163, 184, 0.07);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font: 600 0.84rem/1.3 system-ui, sans-serif;
|
||||
text-align: left;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.anomalous-sources-components-toggle:hover {
|
||||
.anomalous-msrc-site {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 7px;
|
||||
border: 1px solid;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.anomalous-msrc-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: var(--amb-bg-input);
|
||||
color: var(--amb-text-main);
|
||||
background: rgba(148, 163, 184, 0.11);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.anomalous-sources-components-toggle:focus-visible {
|
||||
outline: 2px solid var(--amb-accent, var(--amb-link));
|
||||
outline-offset: 2px;
|
||||
.anomalous-msrc-input:focus {
|
||||
border-color: color-mix(in srgb, var(--amb-link) 55%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-sources-components-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
.anomalous-msrc-row.is-empty .anomalous-msrc-input:not(:focus) {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.anomalous-sources-components-label small {
|
||||
color: var(--amb-text-muted, var(--amb-text-dim));
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
.anomalous-msrc-btn {
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: transparent;
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-sources-components-list {
|
||||
padding: 6px 0 0 14px;
|
||||
border-left: 1px solid rgba(148, 163, 184, 0.2);
|
||||
.anomalous-msrc-btn:hover:not(:disabled) {
|
||||
border-color: var(--amb-border-hover);
|
||||
background: var(--amb-bg-card-hover);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-sources-components-empty {
|
||||
padding: 14px 10px;
|
||||
color: var(--amb-text-dim);
|
||||
.anomalous-msrc-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.anomalous-msrc-btn.is-primary {
|
||||
border-color: transparent;
|
||||
background: var(--amb-btn-primary-bg);
|
||||
color: var(--amb-btn-primary-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-msrc-btn.is-primary:hover:not(:disabled) {
|
||||
background: var(--amb-btn-primary-hover);
|
||||
color: var(--amb-btn-primary-text);
|
||||
}
|
||||
|
||||
.anomalous-msrc-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.anomalous-msrc-components {
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
padding: 4px 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-msrc-components:hover {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-msrc-empty {
|
||||
margin: 18px 0;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.anomalous-msrc-foot {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-msrc-stats {
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.anomalous-msrc-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Narrow (docked): the link goes under the name. */
|
||||
@container (max-width: 640px) {
|
||||
.anomalous-msrc-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user