Migrate frontend sources to TypeScript
This commit is contained in:
@@ -33,6 +33,7 @@ htmlcov/
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
*.egg-info/
|
||||
CHANGELOG.md
|
||||
docs/AUDIT_REPORT.md
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidelines for AI agents working on `ComfyUI-Majoor-ProjectSettings`.
|
||||
|
||||
## Mandatory Upstream Research
|
||||
|
||||
Before answering implementation questions or changing code in this repository, inspect the relevant upstream ComfyUI projects. Do not rely only on memory.
|
||||
|
||||
Use these sources:
|
||||
|
||||
- ComfyUI core: https://github.com/Comfy-Org/ComfyUI
|
||||
- ComfyUI frontend: https://github.com/Comfy-Org/ComfyUI_frontend.git
|
||||
- ComfyUI Manager: https://github.com/Comfy-Org/ComfyUI-Manager
|
||||
- ComfyUI Desktop: https://github.com/Comfy-Org/desktop
|
||||
- Comfy-Org overview: https://github.com/Comfy-Org
|
||||
|
||||
For each task, check the source that owns the behavior you are touching:
|
||||
|
||||
- Extension lifecycle, `app.registerExtension`, graph hooks, node APIs: check ComfyUI core and frontend.
|
||||
- Sidebar tabs, settings UI, dialogs, menus, theme tokens, component conventions: check ComfyUI frontend.
|
||||
- Custom node packaging, installation, Manager metadata, compatibility expectations: check ComfyUI Manager.
|
||||
- Desktop-specific paths, packaged runtime behavior, filesystem constraints: check ComfyUI Desktop.
|
||||
- Cross-repo direction, renamed packages, current repository layout: check the Comfy-Org organization.
|
||||
|
||||
When upstream behavior matters, mention the upstream files or documented patterns used to justify the change.
|
||||
|
||||
## Project Scope
|
||||
|
||||
This custom node must stay aligned with ComfyUI instead of inventing a separate app architecture.
|
||||
|
||||
The extension currently provides:
|
||||
|
||||
- A ComfyUI web extension from `js/`.
|
||||
- Python server routes under `server/`.
|
||||
- No custom ComfyUI nodes.
|
||||
- Project-aware output paths under `output/PROJECTS/<project>/`.
|
||||
- Workflow save, project signature, missing-model tools, and safe path validation.
|
||||
|
||||
Keep that contract unless the user explicitly asks for a breaking redesign.
|
||||
|
||||
## Frontend Rules
|
||||
|
||||
- Prefer ComfyUI frontend APIs and UI patterns over custom framework assumptions.
|
||||
- Keep compatibility with the way ComfyUI loads `WEB_DIRECTORY` assets.
|
||||
- If migrating JavaScript to TypeScript, preserve the generated browser modules expected by ComfyUI.
|
||||
- Do not add Vue, React, Vite, or another framework unless upstream ComfyUI frontend conventions or the requested feature clearly justify it.
|
||||
- If Vue is introduced, document why it is compatible with ComfyUI extension loading and how the build output is served.
|
||||
- Use ComfyUI settings, dialogs, sidebar APIs, and graph hooks when available.
|
||||
- Keep fallback behavior for older ComfyUI versions when this repo already supports it.
|
||||
- Avoid global state leaks; namespace globals with `mjr` or `Majoor`.
|
||||
- Browser code must handle missing or changed ComfyUI APIs defensively.
|
||||
|
||||
## Backend Rules
|
||||
|
||||
- Use ComfyUI server route patterns from upstream.
|
||||
- Keep routes small and validate all inputs at the boundary.
|
||||
- Project paths must remain relative to ComfyUI output/project roots.
|
||||
- Reject absolute paths, drive paths, traversal (`..`), and unsafe separators.
|
||||
- Preserve JSON error responses with `{ "ok": false, "error": "..." }`.
|
||||
- Treat model download, URL fetch, and file write features as security-sensitive.
|
||||
- Keep CSRF, same-origin, API-key, host allowlist, private-IP, and size-limit protections intact.
|
||||
|
||||
## Compatibility Checklist
|
||||
|
||||
Before finishing a code change, verify:
|
||||
|
||||
- ComfyUI can still discover the extension through `WEB_DIRECTORY`.
|
||||
- The main entry module still registers with `app.registerExtension`.
|
||||
- Existing hooks such as `setup`, `beforeConfigureGraph`, `afterConfigureGraph`, `beforeQueuePrompt`, and `nodeCreated` still behave as expected.
|
||||
- Sidebar registration still falls back to the floating panel if needed.
|
||||
- Existing API endpoints are not renamed unless the README and callers are updated together.
|
||||
- Workflow JSON signatures remain backward compatible.
|
||||
- Existing localStorage keys either remain valid or are migrated.
|
||||
|
||||
## Refactor Policy
|
||||
|
||||
- Prefer small, reviewable refactors.
|
||||
- Do not mix a formatting-only rewrite with behavioral changes.
|
||||
- When converting JS to TS, start with type-safe module boundaries and shared domain types.
|
||||
- Keep generated files clearly separated from source files if a build step is added.
|
||||
- Add or update scripts so the repo can rebuild frontend assets reproducibly.
|
||||
- Do not remove plain JS runtime files until ComfyUI loading and packaging are verified.
|
||||
|
||||
## Testing And Verification
|
||||
|
||||
Use the smallest verification that proves the change:
|
||||
|
||||
- Python validation and route logic: run the relevant `pytest` tests.
|
||||
- Frontend build changes: run the TypeScript/build command.
|
||||
- UI behavior changes: test inside ComfyUI when possible.
|
||||
- Path or security changes: add tests for rejected and accepted inputs.
|
||||
- Download or network changes: test error paths and size/host guards.
|
||||
|
||||
If a verification step cannot be run locally, state exactly what was not run and why.
|
||||
|
||||
## Documentation
|
||||
|
||||
Update docs when behavior changes:
|
||||
|
||||
- `README.md` for user-facing features, installation, settings, and endpoints.
|
||||
- `docs/project_structure.json` when project folder roles change.
|
||||
- `AGENTS.md` when development workflow or upstream alignment rules change.
|
||||
|
||||
Keep documentation practical. Include paths, commands, and compatibility notes rather than broad intent.
|
||||
|
||||
## Response Expectations
|
||||
|
||||
When replying to user requests:
|
||||
|
||||
- Answer in the user's language when practical.
|
||||
- Be explicit about which upstream ComfyUI source was checked when it affects the answer.
|
||||
- Prefer implementing requested changes over only proposing them.
|
||||
- Surface compatibility risks early, especially around ComfyUI frontend loading, Manager packaging, Desktop paths, and security-sensitive server routes.
|
||||
@@ -0,0 +1,70 @@
|
||||
# ComfyUI Integration Audit
|
||||
|
||||
Date: 2026-05-26
|
||||
|
||||
## Upstream Sources Checked
|
||||
|
||||
- `Comfy-Org/ComfyUI`
|
||||
- Custom node example confirms `WEB_DIRECTORY` is still the runtime contract for frontend extension assets.
|
||||
- The current core repository hosts compiled frontend assets under `web/`, while frontend sources live separately.
|
||||
- `Comfy-Org/ComfyUI_frontend`
|
||||
- The official frontend is TypeScript/Vue-based.
|
||||
- Extension APIs include `app.registerExtension`, sidebar tabs, commands, keybindings, settings, toast, and desktop-safe dialogs.
|
||||
- Settings should prefer `app.registerExtension({ settings: [...] })` plus `app.extensionManager.setting.get(...)`, with legacy `app.ui.settings` only as fallback.
|
||||
- `Comfy-Org/ComfyUI-Manager`
|
||||
- Custom node packages should not assume their folder name.
|
||||
- Manager and registry workflows expect reproducible package metadata and install-time scripts from the package root.
|
||||
- `Comfy-Org/desktop`
|
||||
- Desktop runs ComfyUI in an Electron environment, so browser-only APIs need ComfyUI dialog/toast fallbacks.
|
||||
|
||||
## Current Repository Findings
|
||||
|
||||
- The extension intentionally exposes no ComfyUI nodes.
|
||||
- `__init__.py` keeps `WEB_DIRECTORY = "./js"`, which is correct for ComfyUI runtime loading.
|
||||
- The frontend was previously maintained directly as browser JavaScript in `js/`.
|
||||
- UI code already uses ComfyUI sidebar registration when available and a floating fallback for older versions.
|
||||
- Dialog helpers already prefer `app.extensionManager.dialog` before browser prompt/confirm, which is aligned with desktop guidance.
|
||||
- Toast helpers already prefer `app.extensionManager.toast`.
|
||||
- Settings registration used the legacy `app.ui.settings.addSetting` path only.
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Added TypeScript source tree under `src/`.
|
||||
- Kept generated runtime modules under `js/` so ComfyUI loading remains unchanged.
|
||||
- Added `package.json`, `package-lock.json`, and `tsconfig.json`.
|
||||
- Added local ComfyUI module declarations in `src/types/comfyui.d.ts`.
|
||||
- Added build scripts:
|
||||
- `npm run build`
|
||||
- `npm run typecheck`
|
||||
- Migrated all previous `js/**/*.js` source files to `src/**/*.ts`.
|
||||
- Rebuilt `js/**/*.js` from TypeScript.
|
||||
- Added modern ComfyUI settings integration through `app.registerExtension({ settings: [...] })`.
|
||||
- Kept a legacy `app.ui.settings.addSetting` fallback for older ComfyUI versions.
|
||||
- Updated setting reads to prefer `app.extensionManager.setting.get(...)`.
|
||||
|
||||
## Vue Decision
|
||||
|
||||
Vue was not introduced in this pass.
|
||||
|
||||
Reason: ComfyUI frontend itself uses Vue internally, but custom node frontend assets are still loaded through `WEB_DIRECTORY` as extension modules. This repository already has a large DOM-built UI and no existing bundler. Introducing Vue would require bundling, dependency packaging, and lifecycle glue without improving compatibility immediately.
|
||||
|
||||
Recommended next step if Vue is desired:
|
||||
|
||||
- Split the Project Settings panel into small UI domains first.
|
||||
- Add a Vite/Vue build only after confirming the generated bundle loads cleanly from `WEB_DIRECTORY`.
|
||||
- Keep the ComfyUI extension entry as a thin adapter around `app.registerExtension`.
|
||||
|
||||
## Remaining Risks
|
||||
|
||||
- TypeScript currently builds with `noCheck: true` because the old JavaScript is highly dynamic and depends on untyped ComfyUI internals.
|
||||
- Generated `.d.ts` files are emitted for future typing work but are not consumed by ComfyUI.
|
||||
- Full browser verification inside ComfyUI is still required after this migration.
|
||||
- The UI is still large and imperative; future refactors should target typed API payloads and smaller panel modules before changing framework.
|
||||
|
||||
## Recommended Next Refactors
|
||||
|
||||
- Add shared TypeScript types for project state, API responses, missing-model scan results, and graph widget shapes.
|
||||
- Tighten `tsconfig.json` gradually by removing `noCheck` module by module.
|
||||
- Move repeated DOM helpers out of `ui_components.ts`.
|
||||
- Consider registering commands/keybindings through ComfyUI's command/keybinding API instead of a raw capture-phase `keydown` listener for `Ctrl+S`.
|
||||
- Add Manager/registry metadata once package publishing is planned.
|
||||
@@ -0,0 +1,73 @@
|
||||
# TypeScript Migration Plan
|
||||
|
||||
Goal: migrate the frontend progressively from permissive TypeScript output to typed, ComfyUI-aligned modules without breaking `WEB_DIRECTORY = "./js"` runtime loading.
|
||||
|
||||
## Phase 0 - Baseline
|
||||
|
||||
- [x] Move frontend source files from `js/**/*.js` to `src/**/*.ts`.
|
||||
- [x] Add reproducible TypeScript build with `npm run build`.
|
||||
- [x] Keep generated browser modules in `js/` for ComfyUI.
|
||||
- [x] Add minimal ComfyUI ambient declarations.
|
||||
- [x] Register settings through modern `app.registerExtension({ settings })`.
|
||||
- [x] Keep legacy settings fallback for older ComfyUI builds.
|
||||
- [x] Document ComfyUI integration audit.
|
||||
- [x] Verify `npm run build`.
|
||||
- [x] Verify current validator tests with `unittest`.
|
||||
|
||||
## Phase 1 - Shared Types
|
||||
|
||||
- [ ] Add typed API response contracts for project list, project set, template preview, workflow save, and model tools.
|
||||
- [ ] Add typed runtime state interfaces for persisted state and transient UI callbacks.
|
||||
- [ ] Add typed graph/node/widget interfaces for the subset used by this extension.
|
||||
- [ ] Replace broad object assumptions in `state_manager.ts`.
|
||||
- [ ] Replace broad API payload assumptions in `mjr/api.ts`.
|
||||
|
||||
## Phase 2 - Low-Risk Strict Modules
|
||||
|
||||
- [ ] Remove `noCheck` from `mjr/utils.ts`.
|
||||
- [ ] Remove `noCheck` from `mjr/log.ts`.
|
||||
- [ ] Remove `noCheck` from `mjr/api.ts`.
|
||||
- [ ] Remove `noCheck` from `state_manager.ts`.
|
||||
- [ ] Enable `noImplicitAny` for the low-risk modules above.
|
||||
|
||||
## Phase 3 - ComfyUI Integration Hardening
|
||||
|
||||
- [ ] Move Ctrl+S behavior from raw `keydown` capture toward ComfyUI command/keybinding APIs when available.
|
||||
- [ ] Keep fallback key handling for older ComfyUI builds.
|
||||
- [ ] Type settings IDs and setting reads.
|
||||
- [ ] Type sidebar render callbacks.
|
||||
- [ ] Type dialog and toast adapter return values.
|
||||
|
||||
## Phase 4 - Graph And Patch Safety
|
||||
|
||||
- [ ] Type graph serialization and project signature objects.
|
||||
- [ ] Type save-like node detection.
|
||||
- [ ] Type path widget mutation.
|
||||
- [ ] Add focused tests or browser fixtures for patching representative save nodes.
|
||||
- [ ] Keep all path security validation server-side.
|
||||
|
||||
## Phase 5 - UI Decomposition
|
||||
|
||||
- [ ] Split `ui_components.ts` into smaller modules by panel area.
|
||||
- [ ] Extract shared DOM helpers.
|
||||
- [ ] Type model fixer dialog state.
|
||||
- [ ] Type model downloader dialog state.
|
||||
- [ ] Type model contribution dialog state.
|
||||
- [ ] Re-evaluate Vue only after UI modules are isolated and build output is stable.
|
||||
|
||||
## Phase 6 - Strict Project
|
||||
|
||||
- [ ] Remove `noCheck` from `tsconfig.json`.
|
||||
- [ ] Enable `strict`.
|
||||
- [ ] Keep `skipLibCheck` unless upstream ComfyUI types are added.
|
||||
- [ ] Run `npm run typecheck` in CI or documented release workflow.
|
||||
- [ ] Document frontend build requirements in `README.md`.
|
||||
|
||||
## Verification Gates
|
||||
|
||||
- [ ] `npm run build`
|
||||
- [ ] `npm run typecheck`
|
||||
- [ ] `python -m unittest discover -s tests -p "test_*.py"`
|
||||
- [ ] Manual ComfyUI browser load of Project Settings tab.
|
||||
- [ ] Manual workflow load/save smoke test.
|
||||
- [ ] Manual save-node patch smoke test.
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Majoor Project Settings (standalone)
|
||||
* - Sidebar UI when available (fallback floating panel)
|
||||
* - No nodes, no polling
|
||||
*/
|
||||
export {};
|
||||
+720
-774
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
export declare function fetchJSON(url: any, opts: any): Promise<any>;
|
||||
export declare function tryResolveProjectId(folder: any): Promise<any>;
|
||||
export declare function listProjects(): Promise<any>;
|
||||
export declare function setProject(project_name: any, create_base?: boolean): Promise<any>;
|
||||
export declare function getModels(): Promise<any>;
|
||||
export declare function createCustomOut(payload: any): Promise<any>;
|
||||
export declare function previewTemplate(template: any, tokens: any): Promise<any>;
|
||||
export declare function listExistingNames(project_id: any, media?: string): Promise<any>;
|
||||
export declare function saveWorkflow(payload: any): Promise<any>;
|
||||
export declare function scanModelCandidates(missing: any): Promise<any>;
|
||||
export declare function resolveModelRecipes(missing: any): Promise<any>;
|
||||
export declare function saveModelRecipes(items: any): Promise<any>;
|
||||
export declare function downloadModels(items: any): Promise<any>;
|
||||
export declare function getDownloadStatus(job_id: any): Promise<any>;
|
||||
export declare function getFingerprintCacheStatus(): Promise<any>;
|
||||
export declare function buildFingerprintCache(payload: any): Promise<any>;
|
||||
export declare function resolveFingerprint(payload: any): Promise<any>;
|
||||
export declare function searchRegistry(query: any, limit?: number): Promise<any>;
|
||||
export declare function contributeRegistry(payload: any): Promise<any>;
|
||||
+152
-171
@@ -1,216 +1,197 @@
|
||||
let _csrfTokenPromise = null;
|
||||
|
||||
function _getApiKey() {
|
||||
try {
|
||||
const key = localStorage.getItem("mjr_api_key");
|
||||
return (key || "").trim();
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function _getCsrfToken(forceRefresh = false) {
|
||||
if (forceRefresh) {
|
||||
_csrfTokenPromise = null;
|
||||
}
|
||||
if (_csrfTokenPromise) return _csrfTokenPromise;
|
||||
_csrfTokenPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetch("/mjr_security/csrf", {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
const key = localStorage.getItem("mjr_api_key");
|
||||
return (key || "").trim();
|
||||
}
|
||||
catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async function _getCsrfToken(forceRefresh = false) {
|
||||
if (forceRefresh) {
|
||||
_csrfTokenPromise = null;
|
||||
}
|
||||
if (_csrfTokenPromise)
|
||||
return _csrfTokenPromise;
|
||||
_csrfTokenPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetch("/mjr_security/csrf", {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
console.error("[MJR] Failed to get CSRF token:", data?.error || `HTTP ${resp.status}`);
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const token = data?.csrf_token || "";
|
||||
if (!token) {
|
||||
console.error("[MJR] CSRF token is empty");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
console.error("[MJR] Failed to get CSRF token:", data?.error || `HTTP ${resp.status}`);
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const token = data?.csrf_token || "";
|
||||
if (!token) {
|
||||
console.error("[MJR] CSRF token is empty");
|
||||
}
|
||||
return token;
|
||||
} catch (err) {
|
||||
console.error("[MJR] Error fetching CSRF token:", err);
|
||||
_csrfTokenPromise = null; // Reset on error
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
return _csrfTokenPromise;
|
||||
catch (err) {
|
||||
console.error("[MJR] Error fetching CSRF token:", err);
|
||||
_csrfTokenPromise = null; // Reset on error
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
return _csrfTokenPromise;
|
||||
}
|
||||
|
||||
export async function fetchJSON(url, opts) {
|
||||
const o = opts ? { ...opts } : {};
|
||||
o.credentials = o.credentials || "same-origin";
|
||||
o.headers = { ...(o.headers || {}) };
|
||||
|
||||
const apiKey = _getApiKey();
|
||||
if (apiKey && !o.headers["X-MJR-API-Key"] && !o.headers["Authorization"]) {
|
||||
o.headers["X-MJR-API-Key"] = apiKey;
|
||||
}
|
||||
|
||||
const method = String(o.method || "GET").toUpperCase();
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
const csrf = await _getCsrfToken();
|
||||
if (csrf) {
|
||||
o.headers["X-CSRF-Token"] = csrf;
|
||||
} else {
|
||||
console.warn("[MJR] No CSRF token available for request");
|
||||
const o = opts ? { ...opts } : {};
|
||||
o.credentials = o.credentials || "same-origin";
|
||||
o.headers = { ...(o.headers || {}) };
|
||||
const apiKey = _getApiKey();
|
||||
if (apiKey && !o.headers["X-MJR-API-Key"] && !o.headers["Authorization"]) {
|
||||
o.headers["X-MJR-API-Key"] = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
const resp = await fetch(url, o);
|
||||
|
||||
// If we get a 403 (Forbidden), it might be a CSRF issue - retry once with fresh token
|
||||
if (resp.status === 403 && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
console.warn("[MJR] Got 403, retrying with fresh CSRF token...");
|
||||
const freshCsrf = await _getCsrfToken(true);
|
||||
if (freshCsrf && freshCsrf !== o.headers["X-CSRF-Token"]) {
|
||||
o.headers["X-CSRF-Token"] = freshCsrf;
|
||||
const retryResp = await fetch(url, o);
|
||||
const retryData = await retryResp.json().catch(() => ({}));
|
||||
if (!retryResp.ok || retryData?.ok === false) {
|
||||
throw new Error(retryData?.error || `HTTP ${retryResp.status}`);
|
||||
}
|
||||
return retryData;
|
||||
const method = String(o.method || "GET").toUpperCase();
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
const csrf = await _getCsrfToken();
|
||||
if (csrf) {
|
||||
o.headers["X-CSRF-Token"] = csrf;
|
||||
}
|
||||
else {
|
||||
console.warn("[MJR] No CSRF token available for request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
return data;
|
||||
const resp = await fetch(url, o);
|
||||
// If we get a 403 (Forbidden), it might be a CSRF issue - retry once with fresh token
|
||||
if (resp.status === 403 && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
console.warn("[MJR] Got 403, retrying with fresh CSRF token...");
|
||||
const freshCsrf = await _getCsrfToken(true);
|
||||
if (freshCsrf && freshCsrf !== o.headers["X-CSRF-Token"]) {
|
||||
o.headers["X-CSRF-Token"] = freshCsrf;
|
||||
const retryResp = await fetch(url, o);
|
||||
const retryData = await retryResp.json().catch(() => ({}));
|
||||
if (!retryResp.ok || retryData?.ok === false) {
|
||||
throw new Error(retryData?.error || `HTTP ${retryResp.status}`);
|
||||
}
|
||||
return retryData;
|
||||
}
|
||||
}
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function tryResolveProjectId(folder) {
|
||||
try {
|
||||
const data = await fetchJSON(`/mjr_project/resolve?folder=${encodeURIComponent(folder)}`);
|
||||
if (data?.ok) return data.project_id || "";
|
||||
} catch (_) {}
|
||||
return "";
|
||||
try {
|
||||
const data = await fetchJSON(`/mjr_project/resolve?folder=${encodeURIComponent(folder)}`);
|
||||
if (data?.ok)
|
||||
return data.project_id || "";
|
||||
}
|
||||
catch (_) { }
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function listProjects() {
|
||||
return fetchJSON("/mjr_project/list");
|
||||
return fetchJSON("/mjr_project/list");
|
||||
}
|
||||
|
||||
export async function setProject(project_name, create_base = true) {
|
||||
return fetchJSON("/mjr_project/set", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_name, create_base }),
|
||||
});
|
||||
return fetchJSON("/mjr_project/set", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_name, create_base }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getModels() {
|
||||
return fetchJSON("/mjr_project/models");
|
||||
return fetchJSON("/mjr_project/models");
|
||||
}
|
||||
|
||||
export async function createCustomOut(payload) {
|
||||
return fetchJSON("/mjr_project/create_custom_out", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
return fetchJSON("/mjr_project/create_custom_out", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function previewTemplate(template, tokens) {
|
||||
return fetchJSON("/mjr_project/preview_template", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ template, tokens: tokens || {} }),
|
||||
});
|
||||
return fetchJSON("/mjr_project/preview_template", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ template, tokens: tokens || {} }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listExistingNames(project_id, media = "images") {
|
||||
const qs = new URLSearchParams({
|
||||
project_id: project_id || "",
|
||||
media: media || "images",
|
||||
});
|
||||
const data = await fetchJSON(`/mjr_project/assets/list_names?${qs.toString()}`);
|
||||
return Array.isArray(data?.names) ? data.names : [];
|
||||
const qs = new URLSearchParams({
|
||||
project_id: project_id || "",
|
||||
media: media || "images",
|
||||
});
|
||||
const data = await fetchJSON(`/mjr_project/assets/list_names?${qs.toString()}`);
|
||||
return Array.isArray(data?.names) ? data.names : [];
|
||||
}
|
||||
|
||||
export async function saveWorkflow(payload) {
|
||||
return fetchJSON("/mjr_project/workflow/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
return fetchJSON("/mjr_project/workflow/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function scanModelCandidates(missing) {
|
||||
return fetchJSON("/mjr_models/scan_candidates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
return fetchJSON("/mjr_models/scan_candidates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveModelRecipes(missing) {
|
||||
return fetchJSON("/mjr_models/resolve_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
return fetchJSON("/mjr_models/resolve_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveModelRecipes(items) {
|
||||
return fetchJSON("/mjr_models/save_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
return fetchJSON("/mjr_models/save_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadModels(items) {
|
||||
return fetchJSON("/mjr_models/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
return fetchJSON("/mjr_models/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDownloadStatus(job_id) {
|
||||
const qs = new URLSearchParams({ job_id: job_id || "" });
|
||||
return fetchJSON(`/mjr_models/download_status?${qs.toString()}`);
|
||||
const qs = new URLSearchParams({ job_id: job_id || "" });
|
||||
return fetchJSON(`/mjr_models/download_status?${qs.toString()}`);
|
||||
}
|
||||
|
||||
export async function getFingerprintCacheStatus() {
|
||||
return fetchJSON("/mjr_models/fingerprint_cache_status");
|
||||
return fetchJSON("/mjr_models/fingerprint_cache_status");
|
||||
}
|
||||
|
||||
export async function buildFingerprintCache(payload) {
|
||||
return fetchJSON("/mjr_models/build_fingerprint_cache", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
return fetchJSON("/mjr_models/build_fingerprint_cache", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveFingerprint(payload) {
|
||||
return fetchJSON("/mjr_models/resolve_by_fingerprint", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
return fetchJSON("/mjr_models/resolve_by_fingerprint", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function searchRegistry(query, limit = 6) {
|
||||
return fetchJSON("/mjr_models/registry/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: String(query || ""), limit }),
|
||||
});
|
||||
return fetchJSON("/mjr_models/registry/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: String(query || ""), limit }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function contributeRegistry(payload) {
|
||||
return fetchJSON("/mjr_models/registry/contribute", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
return fetchJSON("/mjr_models/registry/contribute", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=api.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function psPrompt({ title, message, defaultValue }?: {}): Promise<string>;
|
||||
export declare function psConfirm({ title, message }?: {}): Promise<boolean>;
|
||||
+41
-37
@@ -1,42 +1,46 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
export async function psPrompt({ title, message, defaultValue } = {}) {
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.prompt) {
|
||||
try {
|
||||
const result = await dialog.prompt({
|
||||
title: title || "Input",
|
||||
message: message || "",
|
||||
defaultValue: defaultValue ?? "",
|
||||
default: defaultValue ?? "",
|
||||
});
|
||||
if (result === null || result === undefined) return null;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
const value = result.value;
|
||||
return value == null ? null : String(value);
|
||||
}
|
||||
return String(result);
|
||||
} catch (_) {}
|
||||
}
|
||||
const fallback = window.prompt(message || title || "", defaultValue ?? "");
|
||||
if (fallback === null) return null;
|
||||
return String(fallback);
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.prompt) {
|
||||
try {
|
||||
const result = await dialog.prompt({
|
||||
title: title || "Input",
|
||||
message: message || "",
|
||||
defaultValue: defaultValue ?? "",
|
||||
default: defaultValue ?? "",
|
||||
});
|
||||
if (result === null || result === undefined)
|
||||
return null;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
const value = result.value;
|
||||
return value == null ? null : String(value);
|
||||
}
|
||||
return String(result);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
const fallback = window.prompt(message || title || "", defaultValue ?? "");
|
||||
if (fallback === null)
|
||||
return null;
|
||||
return String(fallback);
|
||||
}
|
||||
|
||||
export async function psConfirm({ title, message } = {}) {
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.confirm) {
|
||||
try {
|
||||
const result = await dialog.confirm({
|
||||
title: title || "Confirm",
|
||||
message: message || "",
|
||||
});
|
||||
if (typeof result === "boolean") return result;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
return Boolean(result.value);
|
||||
}
|
||||
return Boolean(result);
|
||||
} catch (_) {}
|
||||
}
|
||||
return window.confirm(message || title || "Confirm?");
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.confirm) {
|
||||
try {
|
||||
const result = await dialog.confirm({
|
||||
title: title || "Confirm",
|
||||
message: message || "",
|
||||
});
|
||||
if (typeof result === "boolean")
|
||||
return result;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
return Boolean(result.value);
|
||||
}
|
||||
return Boolean(result);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
return window.confirm(message || title || "Confirm?");
|
||||
}
|
||||
//# sourceMappingURL=dialog.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../src/mjr/dialog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAE9C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE;IAClE,MAAM,MAAM,GAAG,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC;IAC7C,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;gBACjC,KAAK,EAAE,KAAK,IAAI,OAAO;gBACvB,OAAO,EAAE,OAAO,IAAI,EAAE;gBACtB,YAAY,EAAE,YAAY,IAAI,EAAE;gBAChC,OAAO,EAAE,YAAY,IAAI,EAAE;aAC5B,CAAC,CAAC;YACH,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC;YACzD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;gBACpD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;gBAC3B,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9C,CAAC;YACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAA,CAAC;IAChB,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,KAAK,IAAI,EAAE,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;IAC3E,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;IACrD,MAAM,MAAM,GAAG,GAAG,EAAE,gBAAgB,EAAE,MAAM,CAAC;IAC7C,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;gBAClC,KAAK,EAAE,KAAK,IAAI,SAAS;gBACzB,OAAO,EAAE,OAAO,IAAI,EAAE;aACvB,CAAC,CAAC;YACH,IAAI,OAAO,MAAM,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAC;YAC/C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;gBACpD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YACD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAA,CAAC;IAChB,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC;AACxD,CAAC"}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
export declare function readGraphSignature(): any;
|
||||
export declare function inferProjectFolderFromGraph(isSaveLikeNodeFn: any): any;
|
||||
export declare function detectModelFromGraph(token3TagFn: any): any;
|
||||
export declare function getSerializedWorkflow(): any;
|
||||
+137
-123
@@ -1,129 +1,143 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
function getInnerGraphFromNode(node) {
|
||||
if (!node) return null;
|
||||
const candidates = [
|
||||
node.subgraph,
|
||||
node._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue;
|
||||
if (Array.isArray(candidate?._nodes)) return candidate;
|
||||
if (Array.isArray(candidate?.graph?._nodes)) return candidate.graph;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function scanGraphForModel(graph, token3TagFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes)) return "";
|
||||
if (visited.has(graph)) return "";
|
||||
visited.add(graph);
|
||||
|
||||
const keys = [
|
||||
"ckpt",
|
||||
"checkpoint",
|
||||
"checkpoint_name",
|
||||
"ckpt_name",
|
||||
"model",
|
||||
"model_name",
|
||||
"diffusion",
|
||||
"diffusion_model",
|
||||
"unet",
|
||||
"unet_name",
|
||||
"gguf",
|
||||
];
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const name = String(w?.name || "").toLowerCase();
|
||||
if (!name) continue;
|
||||
if (!keys.some((k) => name.includes(k))) continue;
|
||||
const val = w?.value;
|
||||
if (typeof val !== "string") continue;
|
||||
const tag = token3TagFn ? token3TagFn(val, false) : "";
|
||||
if (tag) {
|
||||
if (DEBUG) console.debug("[mjr] model tag found", tag);
|
||||
return tag;
|
||||
}
|
||||
if (!node)
|
||||
return null;
|
||||
const candidates = [
|
||||
node.subgraph,
|
||||
node._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate)
|
||||
continue;
|
||||
if (Array.isArray(candidate?._nodes))
|
||||
return candidate;
|
||||
if (Array.isArray(candidate?.graph?._nodes))
|
||||
return candidate.graph;
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG) console.debug("[mjr] scanning subgraph for model");
|
||||
const subTag = scanGraphForModel(inner, token3TagFn, visited);
|
||||
if (subTag) return subTag;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function scanGraphForProjectFolder(graph, isSaveLikeNodeFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes)) return "";
|
||||
if (visited.has(graph)) return "";
|
||||
visited.add(graph);
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (isSaveLikeNodeFn && !isSaveLikeNodeFn(node)) continue;
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const v = w?.value;
|
||||
if (typeof v === "string" || typeof v === "number") {
|
||||
const s = String(v).replace(/\\/g, "/");
|
||||
const m = s.match(/PROJECTS\/([^/]+)/i);
|
||||
if (m && m[1]) return m[1];
|
||||
}
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG) console.debug("[mjr] scanning subgraph for project folder");
|
||||
const subFolder = scanGraphForProjectFolder(inner, isSaveLikeNodeFn, visited);
|
||||
if (subFolder) return subFolder;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function readGraphSignature() {
|
||||
const g = app?.graph;
|
||||
if (!g) return null;
|
||||
const direct = g?.extra?.mjr_project;
|
||||
if (direct && typeof direct === "object") return direct;
|
||||
try {
|
||||
const ser = g?.serialize?.();
|
||||
const sig = ser?.extra?.mjr_project;
|
||||
if (sig && typeof sig === "object") return sig;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function inferProjectFolderFromGraph(isSaveLikeNodeFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForProjectFolder(g, isSaveLikeNodeFn, new WeakSet());
|
||||
}
|
||||
|
||||
export function detectModelFromGraph(token3TagFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForModel(g, token3TagFn, new WeakSet());
|
||||
}
|
||||
|
||||
export function getSerializedWorkflow() {
|
||||
const g = app?.graph;
|
||||
if (!g || typeof g.serialize !== "function") return null;
|
||||
try {
|
||||
const ser = g.serialize();
|
||||
if (g.extra && typeof g.extra === "object") {
|
||||
ser.extra = ser.extra || {};
|
||||
Object.assign(ser.extra, g.extra);
|
||||
}
|
||||
return ser;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function scanGraphForModel(graph, token3TagFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes))
|
||||
return "";
|
||||
if (visited.has(graph))
|
||||
return "";
|
||||
visited.add(graph);
|
||||
const keys = [
|
||||
"ckpt",
|
||||
"checkpoint",
|
||||
"checkpoint_name",
|
||||
"ckpt_name",
|
||||
"model",
|
||||
"model_name",
|
||||
"diffusion",
|
||||
"diffusion_model",
|
||||
"unet",
|
||||
"unet_name",
|
||||
"gguf",
|
||||
];
|
||||
for (const node of graph._nodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const name = String(w?.name || "").toLowerCase();
|
||||
if (!name)
|
||||
continue;
|
||||
if (!keys.some((k) => name.includes(k)))
|
||||
continue;
|
||||
const val = w?.value;
|
||||
if (typeof val !== "string")
|
||||
continue;
|
||||
const tag = token3TagFn ? token3TagFn(val, false) : "";
|
||||
if (tag) {
|
||||
if (DEBUG)
|
||||
console.debug("[mjr] model tag found", tag);
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG)
|
||||
console.debug("[mjr] scanning subgraph for model");
|
||||
const subTag = scanGraphForModel(inner, token3TagFn, visited);
|
||||
if (subTag)
|
||||
return subTag;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function scanGraphForProjectFolder(graph, isSaveLikeNodeFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes))
|
||||
return "";
|
||||
if (visited.has(graph))
|
||||
return "";
|
||||
visited.add(graph);
|
||||
for (const node of graph._nodes) {
|
||||
if (isSaveLikeNodeFn && !isSaveLikeNodeFn(node))
|
||||
continue;
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const v = w?.value;
|
||||
if (typeof v === "string" || typeof v === "number") {
|
||||
const s = String(v).replace(/\\/g, "/");
|
||||
const m = s.match(/PROJECTS\/([^/]+)/i);
|
||||
if (m && m[1])
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG)
|
||||
console.debug("[mjr] scanning subgraph for project folder");
|
||||
const subFolder = scanGraphForProjectFolder(inner, isSaveLikeNodeFn, visited);
|
||||
if (subFolder)
|
||||
return subFolder;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
export function readGraphSignature() {
|
||||
const g = app?.graph;
|
||||
if (!g)
|
||||
return null;
|
||||
const direct = g?.extra?.mjr_project;
|
||||
if (direct && typeof direct === "object")
|
||||
return direct;
|
||||
try {
|
||||
const ser = g?.serialize?.();
|
||||
const sig = ser?.extra?.mjr_project;
|
||||
if (sig && typeof sig === "object")
|
||||
return sig;
|
||||
}
|
||||
catch (_) { }
|
||||
return null;
|
||||
}
|
||||
export function inferProjectFolderFromGraph(isSaveLikeNodeFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForProjectFolder(g, isSaveLikeNodeFn, new WeakSet());
|
||||
}
|
||||
export function detectModelFromGraph(token3TagFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForModel(g, token3TagFn, new WeakSet());
|
||||
}
|
||||
export function getSerializedWorkflow() {
|
||||
const g = app?.graph;
|
||||
if (!g || typeof g.serialize !== "function")
|
||||
return null;
|
||||
try {
|
||||
const ser = g.serialize();
|
||||
if (g.extra && typeof g.extra === "object") {
|
||||
ser.extra = ser.extra || {};
|
||||
Object.assign(ser.extra, g.extra);
|
||||
}
|
||||
return ser;
|
||||
}
|
||||
catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=graph.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/mjr/graph.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAE9C,MAAM,KAAK,GAAG,KAAK,CAAC;AAEpB,SAAS,qBAAqB,CAAC,IAAI;IACjC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,QAAQ;QACb,IAAI,CAAC,SAAS;QACd,IAAI,EAAE,QAAQ,EAAE,KAAK;QACrB,IAAI,EAAE,QAAQ,EAAE,MAAM;QACtB,IAAI,EAAE,UAAU,EAAE,QAAQ;QAC1B,IAAI,CAAC,iBAAiB;QACtB,IAAI,EAAE,iBAAiB,EAAE,KAAK;KAC/B,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS;YAAE,SAAS;QACzB,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;YAAE,OAAO,SAAS,CAAC;QACvD,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC;YAAE,OAAO,SAAS,CAAC,KAAK,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO;IACpD,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACtD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEnB,MAAM,IAAI,GAAG;QACX,MAAM;QACN,YAAY;QACZ,iBAAiB;QACjB,WAAW;QACX,OAAO;QACP,YAAY;QACZ,WAAW;QACX,iBAAiB;QACjB,MAAM;QACN,WAAW;QACX,MAAM;KACP,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAAE,SAAS;YAClD,MAAM,GAAG,GAAG,CAAC,EAAE,KAAK,CAAC;YACrB,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,SAAS;YACtC,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,IAAI,GAAG,EAAE,CAAC;gBACR,IAAI,KAAK;oBAAE,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;gBACvD,OAAO,GAAG,CAAC;YACb,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,KAAK;gBAAE,OAAO,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;YAC9D,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO;IACjE,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACtD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEnB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,gBAAgB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;YAAE,SAAS;QAC1D,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;YACnB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACnD,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBACxC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;gBACxC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;oBAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,KAAK;gBAAE,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;YACvE,MAAM,SAAS,GAAG,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;YAC9E,IAAI,SAAS;gBAAE,OAAO,SAAS,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,MAAM,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC;IACrB,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC;IACrC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC;QACpC,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC;IACjD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,CAAA,CAAC;IACd,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,gBAAgB;IAC1D,MAAM,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC;IACrB,OAAO,yBAAyB,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,OAAO,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAW;IAC9C,MAAM,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC;IACrB,OAAO,iBAAiB,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,MAAM,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC;IACrB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC;QAC1B,IAAI,CAAC,CAAC,KAAK,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC3C,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export declare const MJR_DEBUG: boolean;
|
||||
export declare function debug(...args: any[]): void;
|
||||
export declare function info(...args: any[]): void;
|
||||
export declare function warn(...args: any[]): void;
|
||||
export declare function error(...args: any[]): void;
|
||||
+7
-10
@@ -1,20 +1,17 @@
|
||||
// Simple logging wrapper for Majoor Project Settings UI
|
||||
export const MJR_DEBUG = (typeof window !== 'undefined' && Boolean(window?.MJR_DEBUG)) || false;
|
||||
|
||||
export function debug(...args) {
|
||||
if (MJR_DEBUG) {
|
||||
console.debug(...args);
|
||||
}
|
||||
if (MJR_DEBUG) {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function info(...args) {
|
||||
console.info(...args);
|
||||
console.info(...args);
|
||||
}
|
||||
|
||||
export function warn(...args) {
|
||||
console.warn(...args);
|
||||
console.warn(...args);
|
||||
}
|
||||
|
||||
export function error(...args) {
|
||||
console.error(...args);
|
||||
console.error(...args);
|
||||
}
|
||||
//# sourceMappingURL=log.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/mjr/log.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC;AAEhG,MAAM,UAAU,KAAK,CAAC,GAAG,IAAI;IAC3B,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAG,IAAI;IAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAG,IAAI;IAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,GAAG,IAAI;IAC3B,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AACzB,CAAC"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function showModelContributionDialog(modelName?: string): void;
|
||||
+278
-316
@@ -1,335 +1,297 @@
|
||||
import { ensureStyles, toast } from "./toast.js";
|
||||
import { contributeRegistry } from "./api.js";
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: "huggingface", label: "HuggingFace" },
|
||||
{ value: "civitai", label: "CivitAI" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
{ value: "modelscope", label: "ModelScope" },
|
||||
{ value: "other", label: "Other" },
|
||||
{ value: "huggingface", label: "HuggingFace" },
|
||||
{ value: "civitai", label: "CivitAI" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
{ value: "modelscope", label: "ModelScope" },
|
||||
{ value: "other", label: "Other" },
|
||||
];
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
];
|
||||
|
||||
class ModelContributionDialog {
|
||||
constructor() {
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
}
|
||||
|
||||
show(modelName = "") {
|
||||
this.close();
|
||||
ensureStyles();
|
||||
this._render(modelName);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.dialog) {
|
||||
this.dialog.remove();
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
constructor() {
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
}
|
||||
}
|
||||
|
||||
_render(modelName) {
|
||||
this.dialog = document.createElement("div");
|
||||
this.dialog.className = "mjr-contribution-dialog";
|
||||
Object.assign(this.dialog.style, {
|
||||
position: "fixed",
|
||||
left: "0",
|
||||
top: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "9999",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
Object.assign(overlay.style, {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
background: "rgba(0,0,0,0.65)",
|
||||
backdropFilter: "blur(4px)",
|
||||
});
|
||||
|
||||
const panel = document.createElement("div");
|
||||
Object.assign(panel.style, {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
background: "rgba(15, 18, 26, 0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
borderRadius: "16px",
|
||||
padding: "24px",
|
||||
width: "360px",
|
||||
color: "#f2f4f8",
|
||||
fontFamily: "inherit",
|
||||
pointerEvents: "auto",
|
||||
boxShadow: "0 20px 50px rgba(0,0,0,0.4)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
});
|
||||
|
||||
panel.innerHTML = `
|
||||
show(modelName = "") {
|
||||
this.close();
|
||||
ensureStyles();
|
||||
this._render(modelName);
|
||||
}
|
||||
close() {
|
||||
if (this.dialog) {
|
||||
this.dialog.remove();
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
}
|
||||
}
|
||||
_render(modelName) {
|
||||
this.dialog = document.createElement("div");
|
||||
this.dialog.className = "mjr-contribution-dialog";
|
||||
Object.assign(this.dialog.style, {
|
||||
position: "fixed",
|
||||
left: "0",
|
||||
top: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "9999",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
const overlay = document.createElement("div");
|
||||
Object.assign(overlay.style, {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
background: "rgba(0,0,0,0.65)",
|
||||
backdropFilter: "blur(4px)",
|
||||
});
|
||||
const panel = document.createElement("div");
|
||||
Object.assign(panel.style, {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
background: "rgba(15, 18, 26, 0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
borderRadius: "16px",
|
||||
padding: "24px",
|
||||
width: "360px",
|
||||
color: "#f2f4f8",
|
||||
fontFamily: "inherit",
|
||||
pointerEvents: "auto",
|
||||
boxShadow: "0 20px 50px rgba(0,0,0,0.4)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
});
|
||||
panel.innerHTML = `
|
||||
<h2 style="margin:0;font-size:18px;letter-spacing:0.5px;text-transform:uppercase;">Share a model URL</h2>
|
||||
<p style="margin:0;font-size:12px;color:#a0b3d6;">Help others by contributing a verified download link.</p>
|
||||
`;
|
||||
|
||||
const fields = [
|
||||
{ id: "name", label: "Model name", placeholder: "Stable Diffusion 1.5" },
|
||||
{ id: "url", label: "Download URL", placeholder: "https://huggingface.co/.../model.safetensors" },
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = field.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.placeholder = field.placeholder;
|
||||
input.style.padding = "8px 10px";
|
||||
input.style.borderRadius = "8px";
|
||||
input.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
input.style.background = "rgba(255,255,255,0.04)";
|
||||
input.style.color = "#f4f6fb";
|
||||
input.style.fontSize = "13px";
|
||||
|
||||
if (field.id === "name" && modelName) {
|
||||
input.value = modelName;
|
||||
}
|
||||
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[field.id] = input;
|
||||
const fields = [
|
||||
{ id: "name", label: "Model name", placeholder: "Stable Diffusion 1.5" },
|
||||
{ id: "url", label: "Download URL", placeholder: "https://huggingface.co/.../model.safetensors" },
|
||||
];
|
||||
for (const field of fields) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
const label = document.createElement("label");
|
||||
label.textContent = field.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.placeholder = field.placeholder;
|
||||
input.style.padding = "8px 10px";
|
||||
input.style.borderRadius = "8px";
|
||||
input.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
input.style.background = "rgba(255,255,255,0.04)";
|
||||
input.style.color = "#f4f6fb";
|
||||
input.style.fontSize = "13px";
|
||||
if (field.id === "name" && modelName) {
|
||||
input.value = modelName;
|
||||
}
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[field.id] = input;
|
||||
}
|
||||
const selects = [
|
||||
{ id: "platform", label: "Platform", options: PLATFORM_OPTIONS },
|
||||
{ id: "type", label: "Model type", options: TYPE_OPTIONS },
|
||||
];
|
||||
for (const selectCfg of selects) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
const label = document.createElement("label");
|
||||
label.textContent = selectCfg.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
const select = document.createElement("select");
|
||||
select.style.padding = "8px 10px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
select.style.background = "rgba(255,255,255,0.04)";
|
||||
select.style.color = "#f4f6fb";
|
||||
select.style.fontSize = "13px";
|
||||
for (const option of selectCfg.options) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(select);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[selectCfg.id] = select;
|
||||
}
|
||||
const extraWrap = document.createElement("div");
|
||||
extraWrap.style.display = "flex";
|
||||
extraWrap.style.flexDirection = "column";
|
||||
extraWrap.style.gap = "4px";
|
||||
const fileLabel = document.createElement("label");
|
||||
fileLabel.textContent = "Filename (optional)";
|
||||
fileLabel.style.fontSize = "11px";
|
||||
fileLabel.style.opacity = "0.8";
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "text";
|
||||
fileInput.placeholder = "model.safetensors";
|
||||
Object.assign(fileInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
const shaLabel = document.createElement("label");
|
||||
shaLabel.textContent = "SHA256 (optional)";
|
||||
shaLabel.style.fontSize = "11px";
|
||||
shaLabel.style.opacity = "0.8";
|
||||
const shaInput = document.createElement("input");
|
||||
shaInput.type = "text";
|
||||
shaInput.placeholder = "abc123...";
|
||||
Object.assign(shaInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
extraWrap.appendChild(fileLabel);
|
||||
extraWrap.appendChild(fileInput);
|
||||
extraWrap.appendChild(shaLabel);
|
||||
extraWrap.appendChild(shaInput);
|
||||
panel.appendChild(extraWrap);
|
||||
this.inputs.filename = fileInput;
|
||||
this.inputs.sha256 = shaInput;
|
||||
this.statusEl = document.createElement("div");
|
||||
this.statusEl.style.minHeight = "20px";
|
||||
this.statusEl.style.fontSize = "12px";
|
||||
this.statusEl.style.opacity = "0.85";
|
||||
panel.appendChild(this.statusEl);
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "space-between";
|
||||
this.validateBtn = document.createElement("button");
|
||||
this.validateBtn.textContent = "Validate URL";
|
||||
this.validateBtn.className = "mjr-ps-btn";
|
||||
this.validateBtn.style.flex = "1";
|
||||
this.validateBtn.type = "button";
|
||||
this.submitBtn = document.createElement("button");
|
||||
this.submitBtn.textContent = "Contribute";
|
||||
this.submitBtn.className = "mjr-ps-btn";
|
||||
this.submitBtn.style.flex = "1";
|
||||
this.submitBtn.type = "button";
|
||||
actions.appendChild(this.validateBtn);
|
||||
actions.appendChild(this.submitBtn);
|
||||
panel.appendChild(actions);
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "Cancel";
|
||||
closeBtn.type = "button";
|
||||
closeBtn.style.alignSelf = "flex-end";
|
||||
closeBtn.style.fontSize = "12px";
|
||||
closeBtn.style.background = "transparent";
|
||||
closeBtn.style.border = "none";
|
||||
closeBtn.style.color = "#9ab1d8";
|
||||
panel.appendChild(closeBtn);
|
||||
this.dialog.appendChild(overlay);
|
||||
this.dialog.appendChild(panel);
|
||||
document.body.appendChild(this.dialog);
|
||||
this.validateBtn.addEventListener("click", () => this._validateUrl());
|
||||
this.submitBtn.addEventListener("click", () => this._submit());
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
overlay.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
const selects = [
|
||||
{ id: "platform", label: "Platform", options: PLATFORM_OPTIONS },
|
||||
{ id: "type", label: "Model type", options: TYPE_OPTIONS },
|
||||
];
|
||||
|
||||
for (const selectCfg of selects) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = selectCfg.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.style.padding = "8px 10px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
select.style.background = "rgba(255,255,255,0.04)";
|
||||
select.style.color = "#f4f6fb";
|
||||
select.style.fontSize = "13px";
|
||||
|
||||
for (const option of selectCfg.options) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(select);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[selectCfg.id] = select;
|
||||
_setStatus(message, tone = "info") {
|
||||
if (!this.statusEl)
|
||||
return;
|
||||
this.statusEl.textContent = message;
|
||||
const colors = {
|
||||
info: "#a0b3d6",
|
||||
success: "#84ff9c",
|
||||
error: "#f87c7c",
|
||||
};
|
||||
this.statusEl.style.color = colors[tone] || colors.info;
|
||||
}
|
||||
|
||||
const extraWrap = document.createElement("div");
|
||||
extraWrap.style.display = "flex";
|
||||
extraWrap.style.flexDirection = "column";
|
||||
extraWrap.style.gap = "4px";
|
||||
|
||||
const fileLabel = document.createElement("label");
|
||||
fileLabel.textContent = "Filename (optional)";
|
||||
fileLabel.style.fontSize = "11px";
|
||||
fileLabel.style.opacity = "0.8";
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "text";
|
||||
fileInput.placeholder = "model.safetensors";
|
||||
Object.assign(fileInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
|
||||
const shaLabel = document.createElement("label");
|
||||
shaLabel.textContent = "SHA256 (optional)";
|
||||
shaLabel.style.fontSize = "11px";
|
||||
shaLabel.style.opacity = "0.8";
|
||||
|
||||
const shaInput = document.createElement("input");
|
||||
shaInput.type = "text";
|
||||
shaInput.placeholder = "abc123...";
|
||||
Object.assign(shaInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
|
||||
extraWrap.appendChild(fileLabel);
|
||||
extraWrap.appendChild(fileInput);
|
||||
extraWrap.appendChild(shaLabel);
|
||||
extraWrap.appendChild(shaInput);
|
||||
panel.appendChild(extraWrap);
|
||||
this.inputs.filename = fileInput;
|
||||
this.inputs.sha256 = shaInput;
|
||||
|
||||
this.statusEl = document.createElement("div");
|
||||
this.statusEl.style.minHeight = "20px";
|
||||
this.statusEl.style.fontSize = "12px";
|
||||
this.statusEl.style.opacity = "0.85";
|
||||
panel.appendChild(this.statusEl);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "space-between";
|
||||
|
||||
this.validateBtn = document.createElement("button");
|
||||
this.validateBtn.textContent = "Validate URL";
|
||||
this.validateBtn.className = "mjr-ps-btn";
|
||||
this.validateBtn.style.flex = "1";
|
||||
this.validateBtn.type = "button";
|
||||
|
||||
this.submitBtn = document.createElement("button");
|
||||
this.submitBtn.textContent = "Contribute";
|
||||
this.submitBtn.className = "mjr-ps-btn";
|
||||
this.submitBtn.style.flex = "1";
|
||||
this.submitBtn.type = "button";
|
||||
|
||||
actions.appendChild(this.validateBtn);
|
||||
actions.appendChild(this.submitBtn);
|
||||
panel.appendChild(actions);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "Cancel";
|
||||
closeBtn.type = "button";
|
||||
closeBtn.style.alignSelf = "flex-end";
|
||||
closeBtn.style.fontSize = "12px";
|
||||
closeBtn.style.background = "transparent";
|
||||
closeBtn.style.border = "none";
|
||||
closeBtn.style.color = "#9ab1d8";
|
||||
|
||||
panel.appendChild(closeBtn);
|
||||
|
||||
this.dialog.appendChild(overlay);
|
||||
this.dialog.appendChild(panel);
|
||||
document.body.appendChild(this.dialog);
|
||||
|
||||
this.validateBtn.addEventListener("click", () => this._validateUrl());
|
||||
this.submitBtn.addEventListener("click", () => this._submit());
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
overlay.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
_setStatus(message, tone = "info") {
|
||||
if (!this.statusEl) return;
|
||||
this.statusEl.textContent = message;
|
||||
const colors = {
|
||||
info: "#a0b3d6",
|
||||
success: "#84ff9c",
|
||||
error: "#f87c7c",
|
||||
};
|
||||
this.statusEl.style.color = colors[tone] || colors.info;
|
||||
}
|
||||
|
||||
_validateUrl() {
|
||||
const value = (this.inputs.url?.value || "").trim();
|
||||
if (!value) {
|
||||
this._setStatus("Enter the download URL first.", "error");
|
||||
return;
|
||||
_validateUrl() {
|
||||
const value = (this.inputs.url?.value || "").trim();
|
||||
if (!value) {
|
||||
this._setStatus("Enter the download URL first.", "error");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
this._setStatus("URL must start with http:// or https://", "error");
|
||||
return;
|
||||
}
|
||||
this._setStatus("URL format looks good. You can submit!", "success");
|
||||
}
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
this._setStatus("URL must start with http:// or https://", "error");
|
||||
return;
|
||||
async _submit() {
|
||||
if (!this.submitBtn || !this.dialog)
|
||||
return;
|
||||
const name = (this.inputs.name?.value || "").trim();
|
||||
const url = (this.inputs.url?.value || "").trim();
|
||||
if (!name || !url) {
|
||||
this._setStatus("Model name and URL are required.", "error");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
this._setStatus("URL must start with http(s)", "error");
|
||||
return;
|
||||
}
|
||||
this.submitBtn.disabled = true;
|
||||
this.validateBtn.disabled = true;
|
||||
this._setStatus("Sending contribution...", "info");
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
url,
|
||||
platform: this.inputs.platform?.value || "unknown",
|
||||
type: this.inputs.type?.value || "checkpoints",
|
||||
filename: (this.inputs.filename?.value || "").trim(),
|
||||
sha256: (this.inputs.sha256?.value || "").trim(),
|
||||
};
|
||||
const resp = await contributeRegistry(payload);
|
||||
const added = resp?.added || false;
|
||||
this._setStatus(added ? "Contribution recorded, thanks!" : "Source already exists.", "success");
|
||||
toast("success", "Contribution", added ? "Thank you for sharing!" : "Source already in registry.");
|
||||
if (added) {
|
||||
setTimeout(() => this.close(), 1600);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Contribution failed", error);
|
||||
this._setStatus("Failed to send contribution.", "error");
|
||||
toast("error", "Contribution failed", String(error?.message || error));
|
||||
}
|
||||
finally {
|
||||
this.submitBtn.disabled = false;
|
||||
this.validateBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
this._setStatus("URL format looks good. You can submit!", "success");
|
||||
}
|
||||
|
||||
async _submit() {
|
||||
if (!this.submitBtn || !this.dialog) return;
|
||||
const name = (this.inputs.name?.value || "").trim();
|
||||
const url = (this.inputs.url?.value || "").trim();
|
||||
if (!name || !url) {
|
||||
this._setStatus("Model name and URL are required.", "error");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
this._setStatus("URL must start with http(s)", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitBtn.disabled = true;
|
||||
this.validateBtn.disabled = true;
|
||||
this._setStatus("Sending contribution...", "info");
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
url,
|
||||
platform: this.inputs.platform?.value || "unknown",
|
||||
type: this.inputs.type?.value || "checkpoints",
|
||||
filename: (this.inputs.filename?.value || "").trim(),
|
||||
sha256: (this.inputs.sha256?.value || "").trim(),
|
||||
};
|
||||
const resp = await contributeRegistry(payload);
|
||||
const added = resp?.added || false;
|
||||
this._setStatus(added ? "Contribution recorded, thanks!" : "Source already exists.", "success");
|
||||
toast("success", "Contribution", added ? "Thank you for sharing!" : "Source already in registry.");
|
||||
if (added) {
|
||||
setTimeout(() => this.close(), 1600);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Contribution failed", error);
|
||||
this._setStatus("Failed to send contribution.", "error");
|
||||
toast("error", "Contribution failed", String(error?.message || error));
|
||||
} finally {
|
||||
this.submitBtn.disabled = false;
|
||||
this.validateBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _dialogInstance = null;
|
||||
|
||||
export function showModelContributionDialog(modelName = "") {
|
||||
if (!_dialogInstance) {
|
||||
_dialogInstance = new ModelContributionDialog();
|
||||
}
|
||||
_dialogInstance.show(modelName);
|
||||
if (!_dialogInstance) {
|
||||
_dialogInstance = new ModelContributionDialog();
|
||||
}
|
||||
_dialogInstance.show(modelName);
|
||||
}
|
||||
//# sourceMappingURL=model_contribution_dialog.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
export declare function getKindOptions(): {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
export declare function typeHintToKind(typeHint: any): any;
|
||||
export declare function getAllowedExtensions(): string[];
|
||||
export declare function isValidUrl(url: any): boolean;
|
||||
export declare function extractFilenameFromUrl(url: any): string;
|
||||
export declare function hasAllowedExtension(filename: any): boolean;
|
||||
export declare function normalizeKey(value: any): string;
|
||||
export declare function collectNoteRecipes(workflowJson: any): Map<any, any>;
|
||||
+51
-59
@@ -1,81 +1,73 @@
|
||||
const KIND_OPTIONS = [
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "diffusion_models", label: "Diffusion Model" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "text_encoders", label: "Text Encoder" },
|
||||
{ value: "clip", label: "CLIP" },
|
||||
{ value: "clip_vision", label: "CLIP Vision" },
|
||||
{ value: "unet", label: "UNet" },
|
||||
{ value: "upscale_models", label: "Upscale Model" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "diffusion_models", label: "Diffusion Model" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "text_encoders", label: "Text Encoder" },
|
||||
{ value: "clip", label: "CLIP" },
|
||||
{ value: "clip_vision", label: "CLIP Vision" },
|
||||
{ value: "unet", label: "UNet" },
|
||||
{ value: "upscale_models", label: "Upscale Model" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
];
|
||||
|
||||
const TYPE_HINT_KIND = {
|
||||
checkpoint: "checkpoints",
|
||||
diffusion: "diffusion_models",
|
||||
diffusion_models: "diffusion_models",
|
||||
lora: "loras",
|
||||
vae: "vae",
|
||||
controlnet: "controlnet",
|
||||
upscale_models: "upscale_models",
|
||||
text_encoder: "text_encoders",
|
||||
text_encoders: "text_encoders",
|
||||
clip: "clip",
|
||||
clip_vision: "clip_vision",
|
||||
unet: "unet",
|
||||
embeddings: "embeddings",
|
||||
unknown: "",
|
||||
checkpoint: "checkpoints",
|
||||
diffusion: "diffusion_models",
|
||||
diffusion_models: "diffusion_models",
|
||||
lora: "loras",
|
||||
vae: "vae",
|
||||
controlnet: "controlnet",
|
||||
upscale_models: "upscale_models",
|
||||
text_encoder: "text_encoders",
|
||||
text_encoders: "text_encoders",
|
||||
clip: "clip",
|
||||
clip_vision: "clip_vision",
|
||||
unet: "unet",
|
||||
embeddings: "embeddings",
|
||||
unknown: "",
|
||||
};
|
||||
|
||||
import { collectRecipesFromWorkflowNotes } from "./workflow_note_recipes.js";
|
||||
|
||||
const ALLOWED_EXTENSIONS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin"];
|
||||
|
||||
export function getKindOptions() {
|
||||
return KIND_OPTIONS.slice();
|
||||
return KIND_OPTIONS.slice();
|
||||
}
|
||||
|
||||
export function typeHintToKind(typeHint) {
|
||||
const key = String(typeHint || "").toLowerCase();
|
||||
return TYPE_HINT_KIND[key] || "";
|
||||
const key = String(typeHint || "").toLowerCase();
|
||||
return TYPE_HINT_KIND[key] || "";
|
||||
}
|
||||
|
||||
export function getAllowedExtensions() {
|
||||
return ALLOWED_EXTENSIONS.slice();
|
||||
return ALLOWED_EXTENSIONS.slice();
|
||||
}
|
||||
|
||||
export function isValidUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
}
|
||||
catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFilenameFromUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
}
|
||||
catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAllowedExtension(filename) {
|
||||
const name = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||
const name = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||
}
|
||||
|
||||
export function normalizeKey(value) {
|
||||
const text = String(value || "").replace(/\\/g, "/");
|
||||
const base = text.split("/").pop() || "";
|
||||
return base.split("?")[0].split("#")[0];
|
||||
const text = String(value || "").replace(/\\/g, "/");
|
||||
const base = text.split("/").pop() || "";
|
||||
return base.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
export function collectNoteRecipes(workflowJson) {
|
||||
return collectRecipesFromWorkflowNotes(workflowJson);
|
||||
return collectRecipesFromWorkflowNotes(workflowJson);
|
||||
}
|
||||
//# sourceMappingURL=model_downloader.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"model_downloader.js","sourceRoot":"","sources":["../../src/mjr/model_downloader.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG;IACnB,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE;IAC7C,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACvD,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE;IACjC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE;IAC9B,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE;IAC5C,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE;IACjD,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;IAChC,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,EAAE;IAC9C,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;IAChC,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,eAAe,EAAE;IACnD,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE;CAC5C,CAAC;AAEF,MAAM,cAAc,GAAG;IACrB,UAAU,EAAE,aAAa;IACzB,SAAS,EAAE,kBAAkB;IAC7B,gBAAgB,EAAE,kBAAkB;IACpC,IAAI,EAAE,OAAO;IACb,GAAG,EAAE,KAAK;IACV,UAAU,EAAE,YAAY;IACxB,cAAc,EAAE,gBAAgB;IAChC,YAAY,EAAE,eAAe;IAC7B,aAAa,EAAE,eAAe;IAC9B,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,aAAa;IAC1B,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,YAAY;IACxB,OAAO,EAAE,EAAE;CACZ,CAAC;AAEF,OAAO,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,MAAM,kBAAkB,GAAG,CAAC,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5E,MAAM,UAAU,cAAc;IAC5B,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAQ;IACrC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,kBAAkB,CAAC,KAAK,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAG;IAC5B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;IACrE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAG;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAQ;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAK;IAChC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IACzC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,YAAY;IAC7C,OAAO,+BAA+B,CAAC,YAAY,CAAC,CAAC;AACvD,CAAC"}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export declare function scanMissingModelsFromGraph(): any[];
|
||||
export declare function scanMissingModelsWithStats(): {
|
||||
missing: any[];
|
||||
total: number;
|
||||
};
|
||||
export declare function applyFixesToGraph(fixes: any): number;
|
||||
+296
-290
@@ -1,331 +1,337 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const MAX_MISSING = 200;
|
||||
const VALUE_SET_THRESHOLD = 256;
|
||||
|
||||
// Widgets to exclude from model detection (images, etc.)
|
||||
const IMAGE_WIDGET_NAMES = new Set([
|
||||
"image",
|
||||
"images",
|
||||
"input_image",
|
||||
"image_path",
|
||||
"mask",
|
||||
"background",
|
||||
"foreground",
|
||||
"image",
|
||||
"images",
|
||||
"input_image",
|
||||
"image_path",
|
||||
"mask",
|
||||
"background",
|
||||
"foreground",
|
||||
]);
|
||||
|
||||
const FILE_WIDGET_NAME_HINTS = [
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"mask",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"directory",
|
||||
"folder",
|
||||
"output",
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"mask",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"directory",
|
||||
"folder",
|
||||
"output",
|
||||
];
|
||||
|
||||
const FILE_EXTENSIONS = [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".webp",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".mp4",
|
||||
".webm",
|
||||
".mov",
|
||||
".mkv",
|
||||
".avi",
|
||||
".mp3",
|
||||
".wav",
|
||||
".flac",
|
||||
".ogg",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".webp",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".mp4",
|
||||
".webm",
|
||||
".mov",
|
||||
".mkv",
|
||||
".avi",
|
||||
".mp3",
|
||||
".wav",
|
||||
".flac",
|
||||
".ogg",
|
||||
];
|
||||
|
||||
function isImageWidget(widgetName) {
|
||||
const lower = String(widgetName || "").toLowerCase();
|
||||
if (IMAGE_WIDGET_NAMES.has(lower)) return true;
|
||||
if (lower.includes("png") || lower.includes("jpg") || lower.includes("jpeg")) return true;
|
||||
return false;
|
||||
const lower = String(widgetName || "").toLowerCase();
|
||||
if (IMAGE_WIDGET_NAMES.has(lower))
|
||||
return true;
|
||||
if (lower.includes("png") || lower.includes("jpg") || lower.includes("jpeg"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function looksLikeFileWidget(widgetName, value) {
|
||||
const name = String(widgetName || "").toLowerCase();
|
||||
if (FILE_WIDGET_NAME_HINTS.some((h) => name.includes(h))) return true;
|
||||
const v = String(value || "").toLowerCase().trim();
|
||||
return FILE_EXTENSIONS.some((ext) => v.endsWith(ext));
|
||||
const name = String(widgetName || "").toLowerCase();
|
||||
if (FILE_WIDGET_NAME_HINTS.some((h) => name.includes(h)))
|
||||
return true;
|
||||
const v = String(value || "").toLowerCase().trim();
|
||||
return FILE_EXTENSIONS.some((ext) => v.endsWith(ext));
|
||||
}
|
||||
|
||||
function typeHintFromNode(node, widgetName) {
|
||||
const nodeType = String(node?.type || node?.title || "").toLowerCase();
|
||||
const widgetLower = String(widgetName || "").toLowerCase();
|
||||
const nodeHasClipVision =
|
||||
nodeType.includes("clip_vision") ||
|
||||
nodeType.includes("clip vision") ||
|
||||
(nodeType.includes("clip") && nodeType.includes("vision"));
|
||||
|
||||
// Match based on node type (highest priority)
|
||||
if (nodeHasClipVision) return "clip_vision";
|
||||
if (nodeType.includes("checkpoint")) {
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
return "checkpoint";
|
||||
}
|
||||
if (nodeType.includes("loader")) {
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
if (widgetLower.includes("unet") || widgetLower.includes("diffusion")) return "unet";
|
||||
const nodeType = String(node?.type || node?.title || "").toLowerCase();
|
||||
const widgetLower = String(widgetName || "").toLowerCase();
|
||||
const nodeHasClipVision = nodeType.includes("clip_vision") ||
|
||||
nodeType.includes("clip vision") ||
|
||||
(nodeType.includes("clip") && nodeType.includes("vision"));
|
||||
// Match based on node type (highest priority)
|
||||
if (nodeHasClipVision)
|
||||
return "clip_vision";
|
||||
if (nodeType.includes("checkpoint")) {
|
||||
if (widgetLower.includes("vae"))
|
||||
return "vae";
|
||||
if (widgetLower.includes("lora"))
|
||||
return "lora";
|
||||
return "checkpoint";
|
||||
}
|
||||
if (nodeType.includes("loader")) {
|
||||
if (widgetLower.includes("vae"))
|
||||
return "vae";
|
||||
if (widgetLower.includes("lora"))
|
||||
return "lora";
|
||||
if (widgetLower.includes("unet") || widgetLower.includes("diffusion"))
|
||||
return "unet";
|
||||
if (widgetLower.includes("text_encoder") || widgetLower.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("clip"))
|
||||
return "clip";
|
||||
}
|
||||
if (nodeType.includes("lora"))
|
||||
return "lora";
|
||||
if (nodeType.includes("vae"))
|
||||
return "vae";
|
||||
if (nodeType.includes("controlnet") || nodeType.includes("control"))
|
||||
return "controlnet";
|
||||
if (nodeType.includes("upscale"))
|
||||
return "upscale_models";
|
||||
if (nodeType.includes("clip")) {
|
||||
if (widgetLower.includes("vision"))
|
||||
return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (nodeType.includes("text_encoder") || nodeType.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (nodeType.includes("unet"))
|
||||
return "unet";
|
||||
if (nodeType.includes("embedding") || nodeType.includes("textual"))
|
||||
return "embeddings";
|
||||
// Match based on widget name (secondary priority)
|
||||
if (widgetLower.includes("ckpt") || widgetLower.includes("checkpoint"))
|
||||
return "checkpoint";
|
||||
if (widgetLower.includes("lora"))
|
||||
return "lora";
|
||||
if (widgetLower.includes("vae"))
|
||||
return "vae";
|
||||
if (widgetLower.includes("controlnet") || widgetLower.includes("control"))
|
||||
return "controlnet";
|
||||
if (widgetLower.includes("upscale"))
|
||||
return "upscale_models";
|
||||
if (widgetLower.includes("clip")) {
|
||||
if (widgetLower.includes("vision"))
|
||||
return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (widgetLower.includes("text_encoder") || widgetLower.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("clip")) return "clip";
|
||||
}
|
||||
if (nodeType.includes("lora")) return "lora";
|
||||
if (nodeType.includes("vae")) return "vae";
|
||||
if (nodeType.includes("controlnet") || nodeType.includes("control")) return "controlnet";
|
||||
if (nodeType.includes("upscale")) return "upscale_models";
|
||||
if (nodeType.includes("clip")) {
|
||||
if (widgetLower.includes("vision")) return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (nodeType.includes("text_encoder") || nodeType.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (nodeType.includes("unet")) return "unet";
|
||||
if (nodeType.includes("embedding") || nodeType.includes("textual")) return "embeddings";
|
||||
|
||||
// Match based on widget name (secondary priority)
|
||||
if (widgetLower.includes("ckpt") || widgetLower.includes("checkpoint")) return "checkpoint";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("controlnet") || widgetLower.includes("control")) return "controlnet";
|
||||
if (widgetLower.includes("upscale")) return "upscale_models";
|
||||
if (widgetLower.includes("clip")) {
|
||||
if (widgetLower.includes("vision")) return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (widgetLower.includes("text_encoder") || widgetLower.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("diffusion")) return "diffusion_models";
|
||||
if (widgetLower.includes("unet")) return "unet";
|
||||
if (widgetLower.includes("embedding")) return "embeddings";
|
||||
|
||||
return "unknown";
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("diffusion"))
|
||||
return "diffusion_models";
|
||||
if (widgetLower.includes("unet"))
|
||||
return "unet";
|
||||
if (widgetLower.includes("embedding"))
|
||||
return "embeddings";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function isModelWidget(node, widgetName, value) {
|
||||
if (isImageWidget(widgetName)) return false;
|
||||
if (looksLikeFileWidget(widgetName, value)) return false;
|
||||
return typeHintFromNode(node, widgetName) !== "unknown";
|
||||
if (isImageWidget(widgetName))
|
||||
return false;
|
||||
if (looksLikeFileWidget(widgetName, value))
|
||||
return false;
|
||||
return typeHintFromNode(node, widgetName) !== "unknown";
|
||||
}
|
||||
|
||||
// Recursively collect all nodes including those in subgraphs and groups
|
||||
function collectAllNodes(graph) {
|
||||
const allNodes = [];
|
||||
const visited = new WeakSet();
|
||||
|
||||
function collectFromGraph(g) {
|
||||
if (!g || !Array.isArray(g._nodes)) return;
|
||||
|
||||
for (const node of g._nodes) {
|
||||
if (!node || typeof node !== "object") continue;
|
||||
if (visited.has(node)) continue;
|
||||
visited.add(node);
|
||||
allNodes.push(node);
|
||||
|
||||
// Check for subgraph
|
||||
if (node.subgraph && typeof node.subgraph === "object") {
|
||||
collectFromGraph(node.subgraph);
|
||||
}
|
||||
|
||||
// Check for group nodes (some implementations)
|
||||
if (node.graph && typeof node.graph === "object" && node.graph !== g) {
|
||||
collectFromGraph(node.graph);
|
||||
}
|
||||
const allNodes = [];
|
||||
const visited = new WeakSet();
|
||||
function collectFromGraph(g) {
|
||||
if (!g || !Array.isArray(g._nodes))
|
||||
return;
|
||||
for (const node of g._nodes) {
|
||||
if (!node || typeof node !== "object")
|
||||
continue;
|
||||
if (visited.has(node))
|
||||
continue;
|
||||
visited.add(node);
|
||||
allNodes.push(node);
|
||||
// Check for subgraph
|
||||
if (node.subgraph && typeof node.subgraph === "object") {
|
||||
collectFromGraph(node.subgraph);
|
||||
}
|
||||
// Check for group nodes (some implementations)
|
||||
if (node.graph && typeof node.graph === "object" && node.graph !== g) {
|
||||
collectFromGraph(node.graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectFromGraph(graph);
|
||||
return allNodes;
|
||||
collectFromGraph(graph);
|
||||
return allNodes;
|
||||
}
|
||||
|
||||
function _valuesContain(values, value, widget) {
|
||||
if (!Array.isArray(values) || values.length === 0) return false;
|
||||
|
||||
const valueStr = String(value);
|
||||
if (values.length < VALUE_SET_THRESHOLD) {
|
||||
for (const item of values) {
|
||||
if (String(item) === valueStr) return true;
|
||||
if (!Array.isArray(values) || values.length === 0)
|
||||
return false;
|
||||
const valueStr = String(value);
|
||||
if (values.length < VALUE_SET_THRESHOLD) {
|
||||
for (const item of values) {
|
||||
if (String(item) === valueStr)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!widget || typeof widget !== "object") {
|
||||
const set = new Set(values.map((v) => String(v)));
|
||||
return set.has(valueStr);
|
||||
}
|
||||
|
||||
if (widget.__mjrValuesRef !== values || !(widget.__mjrValuesSet instanceof Set)) {
|
||||
widget.__mjrValuesRef = values;
|
||||
widget.__mjrValuesSet = new Set(values.map((v) => String(v)));
|
||||
}
|
||||
return widget.__mjrValuesSet.has(valueStr);
|
||||
if (!widget || typeof widget !== "object") {
|
||||
const set = new Set(values.map((v) => String(v)));
|
||||
return set.has(valueStr);
|
||||
}
|
||||
if (widget.__mjrValuesRef !== values || !(widget.__mjrValuesSet instanceof Set)) {
|
||||
widget.__mjrValuesRef = values;
|
||||
widget.__mjrValuesSet = new Set(values.map((v) => String(v)));
|
||||
}
|
||||
return widget.__mjrValuesSet.has(valueStr);
|
||||
}
|
||||
|
||||
export function scanMissingModelsFromGraph() {
|
||||
return scanMissingModelsWithStats().missing;
|
||||
return scanMissingModelsWithStats().missing;
|
||||
}
|
||||
|
||||
export function scanMissingModelsWithStats() {
|
||||
const graph = app?.graph;
|
||||
if (!graph) return { missing: [], total: 0 };
|
||||
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
|
||||
const missing = [];
|
||||
const nodesWithMissing = new Set();
|
||||
let total = 0;
|
||||
|
||||
for (const node of allNodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const widget of widgets) {
|
||||
const widgetName = widget?.name || "";
|
||||
|
||||
const values = widget?.options?.values;
|
||||
if (!Array.isArray(values)) continue;
|
||||
const value = widget?.value;
|
||||
if (!value) continue;
|
||||
|
||||
// Only scan model selector widgets (skip image/video/file/path widgets, etc.)
|
||||
if (!isModelWidget(node, widgetName, value)) continue;
|
||||
|
||||
total += 1;
|
||||
if (_valuesContain(values, value, widget)) continue;
|
||||
|
||||
missing.push({
|
||||
node_id: node?.id ?? null,
|
||||
node_title: node?.title || node?.type || "",
|
||||
widget_name: widgetName,
|
||||
missing_value: String(value),
|
||||
type_hint: typeHintFromNode(node, widgetName),
|
||||
});
|
||||
|
||||
// Track nodes with missing models
|
||||
if (node?.id != null) {
|
||||
nodesWithMissing.add(node.id);
|
||||
}
|
||||
|
||||
if (missing.length >= MAX_MISSING) break;
|
||||
const graph = app?.graph;
|
||||
if (!graph)
|
||||
return { missing: [], total: 0 };
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
const missing = [];
|
||||
const nodesWithMissing = new Set();
|
||||
let total = 0;
|
||||
for (const node of allNodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const widget of widgets) {
|
||||
const widgetName = widget?.name || "";
|
||||
const values = widget?.options?.values;
|
||||
if (!Array.isArray(values))
|
||||
continue;
|
||||
const value = widget?.value;
|
||||
if (!value)
|
||||
continue;
|
||||
// Only scan model selector widgets (skip image/video/file/path widgets, etc.)
|
||||
if (!isModelWidget(node, widgetName, value))
|
||||
continue;
|
||||
total += 1;
|
||||
if (_valuesContain(values, value, widget))
|
||||
continue;
|
||||
missing.push({
|
||||
node_id: node?.id ?? null,
|
||||
node_title: node?.title || node?.type || "",
|
||||
widget_name: widgetName,
|
||||
missing_value: String(value),
|
||||
type_hint: typeHintFromNode(node, widgetName),
|
||||
});
|
||||
// Track nodes with missing models
|
||||
if (node?.id != null) {
|
||||
nodesWithMissing.add(node.id);
|
||||
}
|
||||
if (missing.length >= MAX_MISSING)
|
||||
break;
|
||||
}
|
||||
if (missing.length >= MAX_MISSING)
|
||||
break;
|
||||
}
|
||||
if (missing.length >= MAX_MISSING) break;
|
||||
}
|
||||
|
||||
// Highlight nodes with missing models
|
||||
highlightNodesWithMissingModels(allNodes, nodesWithMissing);
|
||||
|
||||
return { missing, total };
|
||||
// Highlight nodes with missing models
|
||||
highlightNodesWithMissingModels(allNodes, nodesWithMissing);
|
||||
return { missing, total };
|
||||
}
|
||||
|
||||
function highlightNodesWithMissingModels(allNodes, nodesWithMissing) {
|
||||
for (const node of allNodes) {
|
||||
if (!node) continue;
|
||||
|
||||
if (nodesWithMissing.has(node.id)) {
|
||||
if (!node.__mjrMissingHighlight) {
|
||||
node.__mjrMissingHighlight = true;
|
||||
node.__mjrMissingColor = node.color ?? null;
|
||||
node.__mjrMissingBgcolor = node.bgcolor ?? null;
|
||||
}
|
||||
// Highlight node with missing models (border only)
|
||||
node.color = "#FF0000";
|
||||
node.bgcolor = null;
|
||||
} else if (node.__mjrMissingHighlight) {
|
||||
// Restore original colors if no longer missing
|
||||
node.color = node.__mjrMissingColor ?? null;
|
||||
node.bgcolor = node.__mjrMissingBgcolor ?? null;
|
||||
delete node.__mjrMissingHighlight;
|
||||
delete node.__mjrMissingColor;
|
||||
delete node.__mjrMissingBgcolor;
|
||||
for (const node of allNodes) {
|
||||
if (!node)
|
||||
continue;
|
||||
if (nodesWithMissing.has(node.id)) {
|
||||
if (!node.__mjrMissingHighlight) {
|
||||
node.__mjrMissingHighlight = true;
|
||||
node.__mjrMissingColor = node.color ?? null;
|
||||
node.__mjrMissingBgcolor = node.bgcolor ?? null;
|
||||
}
|
||||
// Highlight node with missing models (border only)
|
||||
node.color = "#FF0000";
|
||||
node.bgcolor = null;
|
||||
}
|
||||
else if (node.__mjrMissingHighlight) {
|
||||
// Restore original colors if no longer missing
|
||||
node.color = node.__mjrMissingColor ?? null;
|
||||
node.bgcolor = node.__mjrMissingBgcolor ?? null;
|
||||
delete node.__mjrMissingHighlight;
|
||||
delete node.__mjrMissingColor;
|
||||
delete node.__mjrMissingBgcolor;
|
||||
}
|
||||
}
|
||||
// Force canvas redraw
|
||||
if (typeof app?.graph?.setDirtyCanvas === "function") {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Force canvas redraw
|
||||
if (typeof app?.graph?.setDirtyCanvas === "function") {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyFixesToGraph(fixes) {
|
||||
const graph = app?.graph;
|
||||
if (!graph) return 0;
|
||||
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
|
||||
const nodesById = new Map();
|
||||
for (const node of allNodes) {
|
||||
if (node && node.id != null) {
|
||||
nodesById.set(node.id, node);
|
||||
const graph = app?.graph;
|
||||
if (!graph)
|
||||
return 0;
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
const nodesById = new Map();
|
||||
for (const node of allNodes) {
|
||||
if (node && node.id != null) {
|
||||
nodesById.set(node.id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fixed = 0;
|
||||
const fixedNodeIds = new Set();
|
||||
|
||||
for (const fix of fixes || []) {
|
||||
if (!fix) continue;
|
||||
const node = nodesById.get(fix.node_id);
|
||||
if (!node) continue;
|
||||
const widgets = node?.widgets || [];
|
||||
const widget = widgets.find((w) => w?.name === fix.widget_name);
|
||||
if (!widget) continue;
|
||||
widget.value = fix.new_value;
|
||||
// Ensure widget callbacks fire so nodes update internal state.
|
||||
if (typeof widget.callback === "function") {
|
||||
try {
|
||||
widget.callback(widget.value, app.graph, node, widget);
|
||||
} catch (_) {}
|
||||
let fixed = 0;
|
||||
const fixedNodeIds = new Set();
|
||||
for (const fix of fixes || []) {
|
||||
if (!fix)
|
||||
continue;
|
||||
const node = nodesById.get(fix.node_id);
|
||||
if (!node)
|
||||
continue;
|
||||
const widgets = node?.widgets || [];
|
||||
const widget = widgets.find((w) => w?.name === fix.widget_name);
|
||||
if (!widget)
|
||||
continue;
|
||||
widget.value = fix.new_value;
|
||||
// Ensure widget callbacks fire so nodes update internal state.
|
||||
if (typeof widget.callback === "function") {
|
||||
try {
|
||||
widget.callback(widget.value, app.graph, node, widget);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
if (typeof node.onWidgetChanged === "function") {
|
||||
try {
|
||||
node.onWidgetChanged(widget.name, widget.value, widget);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
if (typeof node.onPropertyChanged === "function") {
|
||||
try {
|
||||
node.onPropertyChanged(widget.name, widget.value, widget);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
if (Array.isArray(node.widgets)) {
|
||||
node.widgets_values = node.widgets.map((w) => w?.value);
|
||||
}
|
||||
if (typeof node.setDirtyCanvas === "function") {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
fixed += 1;
|
||||
fixedNodeIds.add(fix.node_id);
|
||||
}
|
||||
if (typeof node.onWidgetChanged === "function") {
|
||||
try {
|
||||
node.onWidgetChanged(widget.name, widget.value, widget);
|
||||
} catch (_) {}
|
||||
// Clear highlighting on fixed nodes
|
||||
for (const nodeId of fixedNodeIds) {
|
||||
const node = nodesById.get(nodeId);
|
||||
if (node && node.color === "#FF0000") {
|
||||
node.color = null;
|
||||
node.bgcolor = null;
|
||||
}
|
||||
}
|
||||
if (typeof node.onPropertyChanged === "function") {
|
||||
try {
|
||||
node.onPropertyChanged(widget.name, widget.value, widget);
|
||||
} catch (_) {}
|
||||
if (fixed > 0 && typeof graph.setDirtyCanvas === "function") {
|
||||
graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
if (Array.isArray(node.widgets)) {
|
||||
node.widgets_values = node.widgets.map((w) => w?.value);
|
||||
if (fixed > 0 && typeof app?.refreshComboInNodes === "function") {
|
||||
try {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
if (typeof node.setDirtyCanvas === "function") {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
fixed += 1;
|
||||
fixedNodeIds.add(fix.node_id);
|
||||
}
|
||||
|
||||
// Clear highlighting on fixed nodes
|
||||
for (const nodeId of fixedNodeIds) {
|
||||
const node = nodesById.get(nodeId);
|
||||
if (node && node.color === "#FF0000") {
|
||||
node.color = null;
|
||||
node.bgcolor = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixed > 0 && typeof graph.setDirtyCanvas === "function") {
|
||||
graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
if (fixed > 0 && typeof app?.refreshComboInNodes === "function") {
|
||||
try {
|
||||
app.refreshComboInNodes();
|
||||
} catch (_) {}
|
||||
}
|
||||
return fixed;
|
||||
return fixed;
|
||||
}
|
||||
//# sourceMappingURL=model_fixer.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export declare let PATH_WIDGETS: any[];
|
||||
export declare function loadConfig(): Promise<any>;
|
||||
export declare function detectNodeMedia(node: any): "images" | "videos";
|
||||
export declare function isSaveLikeNode(node: any): any;
|
||||
export declare function alreadyProjectPathed(value: any): boolean;
|
||||
export declare function patchSingleNode(node: any, relDir: any, filenamePrefix: any): boolean;
|
||||
export declare function patchSaveNodes(app: any, relDir: any, filenamePrefix: any, targetMedia: any): Promise<number>;
|
||||
export declare function stampGraphProjectSignature(app: any, state: any): void;
|
||||
+88
-90
@@ -1,107 +1,105 @@
|
||||
import { fetchJSON } from "./api.js";
|
||||
import { safeRel, joinRel } from "./utils.js";
|
||||
|
||||
export let PATH_WIDGETS = [];
|
||||
let configPromise = null;
|
||||
|
||||
export async function loadConfig() {
|
||||
if (configPromise) return configPromise;
|
||||
configPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetchJSON("/mjr_project/config");
|
||||
const list = Array.isArray(resp?.path_widgets) ? resp.path_widgets : [];
|
||||
PATH_WIDGETS = list.map((item) => String(item)).filter(Boolean);
|
||||
return PATH_WIDGETS;
|
||||
} catch (err) {
|
||||
console.error("[mjr] Failed to load config:", err);
|
||||
return PATH_WIDGETS;
|
||||
} finally {
|
||||
configPromise = null;
|
||||
}
|
||||
})();
|
||||
return configPromise;
|
||||
if (configPromise)
|
||||
return configPromise;
|
||||
configPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetchJSON("/mjr_project/config");
|
||||
const list = Array.isArray(resp?.path_widgets) ? resp.path_widgets : [];
|
||||
PATH_WIDGETS = list.map((item) => String(item)).filter(Boolean);
|
||||
return PATH_WIDGETS;
|
||||
}
|
||||
catch (err) {
|
||||
console.error("[mjr] Failed to load config:", err);
|
||||
return PATH_WIDGETS;
|
||||
}
|
||||
finally {
|
||||
configPromise = null;
|
||||
}
|
||||
})();
|
||||
return configPromise;
|
||||
}
|
||||
|
||||
export function detectNodeMedia(node) {
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (t.includes("video") || t.includes("mp4") || t.includes("gif")) return "videos";
|
||||
if (t.includes("image") || t.includes("png") || t.includes("jpg") || t.includes("webp"))
|
||||
return "images";
|
||||
|
||||
const widgets = node?.widgets || [];
|
||||
const hasVideoHints = widgets.some((w) =>
|
||||
["frame_rate", "fps", "duration", "frames_per_second"].includes(String(w?.name || ""))
|
||||
);
|
||||
return hasVideoHints ? "videos" : "images";
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (t.includes("video") || t.includes("mp4") || t.includes("gif"))
|
||||
return "videos";
|
||||
if (t.includes("image") || t.includes("png") || t.includes("jpg") || t.includes("webp"))
|
||||
return "images";
|
||||
const widgets = node?.widgets || [];
|
||||
const hasVideoHints = widgets.some((w) => ["frame_rate", "fps", "duration", "frames_per_second"].includes(String(w?.name || "")));
|
||||
return hasVideoHints ? "videos" : "images";
|
||||
}
|
||||
|
||||
export function isSaveLikeNode(node) {
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (!t.includes("save") && !t.includes("export") && !t.includes("combine")) return false;
|
||||
const widgets = node?.widgets || [];
|
||||
const hasFilename = widgets.some((w) => w?.name === "filename_prefix");
|
||||
const hasPath = widgets.some((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
return hasFilename || hasPath;
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (!t.includes("save") && !t.includes("export") && !t.includes("combine"))
|
||||
return false;
|
||||
const widgets = node?.widgets || [];
|
||||
const hasFilename = widgets.some((w) => w?.name === "filename_prefix");
|
||||
const hasPath = widgets.some((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
return hasFilename || hasPath;
|
||||
}
|
||||
|
||||
export function alreadyProjectPathed(value) {
|
||||
if (!value) return false;
|
||||
const s = String(value).replace(/\\/g, "/");
|
||||
return s.includes("PROJECTS/") || s.includes("02_OUT/") || s.includes("00_META/");
|
||||
if (!value)
|
||||
return false;
|
||||
const s = String(value).replace(/\\/g, "/");
|
||||
return s.includes("PROJECTS/") || s.includes("02_OUT/") || s.includes("00_META/");
|
||||
}
|
||||
|
||||
export function patchSingleNode(node, relDir, filenamePrefix) {
|
||||
if (!isSaveLikeNode(node)) return false;
|
||||
|
||||
const rel = safeRel(relDir);
|
||||
const prefix = safeRel(filenamePrefix);
|
||||
const widgets = node?.widgets || [];
|
||||
|
||||
const filenameWidget = widgets.find((w) => w?.name === "filename_prefix");
|
||||
const pathWidget = widgets.find((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
|
||||
if (pathWidget && filenameWidget) {
|
||||
pathWidget.value = rel;
|
||||
filenameWidget.value = prefix;
|
||||
return true;
|
||||
}
|
||||
if (filenameWidget) {
|
||||
filenameWidget.value = prefix ? joinRel(rel, prefix) : rel;
|
||||
return true;
|
||||
}
|
||||
if (pathWidget) {
|
||||
pathWidget.value = rel;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function patchSaveNodes(app, relDir, filenamePrefix, targetMedia) {
|
||||
await loadConfig();
|
||||
const nodes = app?.graph?._nodes || [];
|
||||
let patched = 0;
|
||||
for (const node of nodes) {
|
||||
const nm = detectNodeMedia(node);
|
||||
if (targetMedia && nm && nm !== targetMedia) continue;
|
||||
if (patchSingleNode(node, relDir, filenamePrefix)) {
|
||||
patched += 1;
|
||||
if (!isSaveLikeNode(node))
|
||||
return false;
|
||||
const rel = safeRel(relDir);
|
||||
const prefix = safeRel(filenamePrefix);
|
||||
const widgets = node?.widgets || [];
|
||||
const filenameWidget = widgets.find((w) => w?.name === "filename_prefix");
|
||||
const pathWidget = widgets.find((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
if (pathWidget && filenameWidget) {
|
||||
pathWidget.value = rel;
|
||||
filenameWidget.value = prefix;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
try {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
} catch (_) {}
|
||||
return patched;
|
||||
if (filenameWidget) {
|
||||
filenameWidget.value = prefix ? joinRel(rel, prefix) : rel;
|
||||
return true;
|
||||
}
|
||||
if (pathWidget) {
|
||||
pathWidget.value = rel;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function patchSaveNodes(app, relDir, filenamePrefix, targetMedia) {
|
||||
await loadConfig();
|
||||
const nodes = app?.graph?._nodes || [];
|
||||
let patched = 0;
|
||||
for (const node of nodes) {
|
||||
const nm = detectNodeMedia(node);
|
||||
if (targetMedia && nm && nm !== targetMedia)
|
||||
continue;
|
||||
if (patchSingleNode(node, relDir, filenamePrefix)) {
|
||||
patched += 1;
|
||||
}
|
||||
}
|
||||
try {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
catch (_) { }
|
||||
return patched;
|
||||
}
|
||||
|
||||
export function stampGraphProjectSignature(app, state) {
|
||||
if (!state?.projectId || !state?.projectFolder) return;
|
||||
const g = app?.graph;
|
||||
if (!g) return;
|
||||
g.extra = g.extra || {};
|
||||
g.extra.mjr_project = {
|
||||
project_id: state.projectId,
|
||||
project_folder: state.projectFolder,
|
||||
template: state.pathTemplate || "",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
if (!state?.projectId || !state?.projectFolder)
|
||||
return;
|
||||
const g = app?.graph;
|
||||
if (!g)
|
||||
return;
|
||||
g.extra = g.extra || {};
|
||||
g.extra.mjr_project = {
|
||||
project_id: state.projectId,
|
||||
project_folder: state.projectFolder,
|
||||
template: state.pathTemplate || "",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=patch.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"patch.js","sourceRoot":"","sources":["../../src/mjr/patch.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare function ensureStyles(): void;
|
||||
export declare function toast(type: any, title: any, message: any, opts?: {}): void;
|
||||
+77
-78
@@ -1,10 +1,10 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
export function ensureStyles() {
|
||||
if (document.getElementById("mjr-project-settings-style")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "mjr-project-settings-style";
|
||||
style.textContent = `
|
||||
if (document.getElementById("mjr-project-settings-style"))
|
||||
return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "mjr-project-settings-style";
|
||||
style.textContent = `
|
||||
.mjr-ps-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -46,81 +46,80 @@ export function ensureStyles() {
|
||||
color: #f2f4f8 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
let toastContainer = null;
|
||||
|
||||
function getToastContainer() {
|
||||
if (toastContainer && document.body.contains(toastContainer)) return toastContainer;
|
||||
let el = document.getElementById("mjr-ps-toast-container");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "mjr-ps-toast-container";
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "16px";
|
||||
el.style.right = "16px";
|
||||
el.style.zIndex = "10000";
|
||||
el.style.display = "flex";
|
||||
el.style.flexDirection = "column";
|
||||
el.style.gap = "8px";
|
||||
el.style.pointerEvents = "none";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
toastContainer = el;
|
||||
return el;
|
||||
}
|
||||
|
||||
export function toast(type, title, message, opts = {}) {
|
||||
const map = { success: "success", info: "info", warn: "warn", error: "error" };
|
||||
const severity = map[type] || "info";
|
||||
try {
|
||||
const mgr = app?.extensionManager?.toast;
|
||||
if (mgr?.add) {
|
||||
mgr.add({
|
||||
severity,
|
||||
summary: title || "",
|
||||
detail: message || "",
|
||||
life: Number(opts.life || 4000),
|
||||
});
|
||||
return;
|
||||
if (toastContainer && document.body.contains(toastContainer))
|
||||
return toastContainer;
|
||||
let el = document.getElementById("mjr-ps-toast-container");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "mjr-ps-toast-container";
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "16px";
|
||||
el.style.right = "16px";
|
||||
el.style.zIndex = "10000";
|
||||
el.style.display = "flex";
|
||||
el.style.flexDirection = "column";
|
||||
el.style.gap = "8px";
|
||||
el.style.pointerEvents = "none";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
const container = getToastContainer();
|
||||
const item = document.createElement("div");
|
||||
const colors = {
|
||||
success: "rgba(46, 143, 78, 0.9)",
|
||||
info: "rgba(46, 136, 255, 0.9)",
|
||||
warn: "rgba(199, 124, 42, 0.9)",
|
||||
error: "rgba(179, 58, 58, 0.9)",
|
||||
};
|
||||
item.style.background = colors[severity] || colors.info;
|
||||
item.style.color = "#f2f4f8";
|
||||
item.style.padding = "8px 10px";
|
||||
item.style.borderRadius = "8px";
|
||||
item.style.boxShadow = "0 6px 16px rgba(0,0,0,0.3)";
|
||||
item.style.maxWidth = "320px";
|
||||
item.style.pointerEvents = "none";
|
||||
item.style.backdropFilter = "blur(6px)";
|
||||
|
||||
const strong = document.createElement("div");
|
||||
strong.textContent = title || "";
|
||||
strong.style.fontWeight = "600";
|
||||
strong.style.marginBottom = message ? "2px" : "0";
|
||||
const detail = document.createElement("div");
|
||||
detail.textContent = message || "";
|
||||
detail.style.opacity = "0.9";
|
||||
detail.style.fontSize = "12px";
|
||||
|
||||
item.appendChild(strong);
|
||||
if (message) item.appendChild(detail);
|
||||
container.appendChild(item);
|
||||
|
||||
const life = Number(opts.life || 4000);
|
||||
setTimeout(() => {
|
||||
if (item.parentNode) item.remove();
|
||||
}, life);
|
||||
} catch (_) {}
|
||||
toastContainer = el;
|
||||
return el;
|
||||
}
|
||||
export function toast(type, title, message, opts = {}) {
|
||||
const map = { success: "success", info: "info", warn: "warn", error: "error" };
|
||||
const severity = map[type] || "info";
|
||||
try {
|
||||
const mgr = app?.extensionManager?.toast;
|
||||
if (mgr?.add) {
|
||||
mgr.add({
|
||||
severity,
|
||||
summary: title || "",
|
||||
detail: message || "",
|
||||
life: Number(opts.life || 4000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (_) { }
|
||||
try {
|
||||
const container = getToastContainer();
|
||||
const item = document.createElement("div");
|
||||
const colors = {
|
||||
success: "rgba(46, 143, 78, 0.9)",
|
||||
info: "rgba(46, 136, 255, 0.9)",
|
||||
warn: "rgba(199, 124, 42, 0.9)",
|
||||
error: "rgba(179, 58, 58, 0.9)",
|
||||
};
|
||||
item.style.background = colors[severity] || colors.info;
|
||||
item.style.color = "#f2f4f8";
|
||||
item.style.padding = "8px 10px";
|
||||
item.style.borderRadius = "8px";
|
||||
item.style.boxShadow = "0 6px 16px rgba(0,0,0,0.3)";
|
||||
item.style.maxWidth = "320px";
|
||||
item.style.pointerEvents = "none";
|
||||
item.style.backdropFilter = "blur(6px)";
|
||||
const strong = document.createElement("div");
|
||||
strong.textContent = title || "";
|
||||
strong.style.fontWeight = "600";
|
||||
strong.style.marginBottom = message ? "2px" : "0";
|
||||
const detail = document.createElement("div");
|
||||
detail.textContent = message || "";
|
||||
detail.style.opacity = "0.9";
|
||||
detail.style.fontSize = "12px";
|
||||
item.appendChild(strong);
|
||||
if (message)
|
||||
item.appendChild(detail);
|
||||
container.appendChild(item);
|
||||
const life = Number(opts.life || 4000);
|
||||
setTimeout(() => {
|
||||
if (item.parentNode)
|
||||
item.remove();
|
||||
}, life);
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
//# sourceMappingURL=toast.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"toast.js","sourceRoot":"","sources":["../../src/mjr/toast.ts"],"names":[],"mappings":"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"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function showModelDownloaderDialog({ entries, kindOptions, existingMap }?: {}): Promise<unknown>;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function showModelFixerDialog({ missing, results }?: {}): Promise<unknown>;
|
||||
+294
-324
@@ -1,335 +1,305 @@
|
||||
import { ensureStyles } from "./toast.js";
|
||||
|
||||
function buildResultMap(results) {
|
||||
const map = new Map();
|
||||
for (const entry of results || []) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
map.set(key, {
|
||||
candidates: Array.isArray(entry?.candidates) ? entry.candidates : [],
|
||||
exact_match_wrong_folder: entry?.exact_match_wrong_folder || null,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
const map = new Map();
|
||||
for (const entry of results || []) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
map.set(key, {
|
||||
candidates: Array.isArray(entry?.candidates) ? entry.candidates : [],
|
||||
exact_match_wrong_folder: entry?.exact_match_wrong_folder || null,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function formatCandidateLabel(candidate) {
|
||||
const score = Number(candidate?.score || 0);
|
||||
const basename = String(candidate?.basename || candidate?.relpath || "");
|
||||
const kind = String(candidate?.kind || "");
|
||||
if (kind) {
|
||||
return `${score} | ${basename} (${kind})`;
|
||||
}
|
||||
return `${score} | ${basename}`;
|
||||
const score = Number(candidate?.score || 0);
|
||||
const basename = String(candidate?.basename || candidate?.relpath || "");
|
||||
const kind = String(candidate?.kind || "");
|
||||
if (kind) {
|
||||
return `${score} | ${basename} (${kind})`;
|
||||
}
|
||||
return `${score} | ${basename}`;
|
||||
}
|
||||
|
||||
export function showModelFixerDialog({ missing, results } = {}) {
|
||||
ensureStyles();
|
||||
const missingList = Array.isArray(missing) ? missing : [];
|
||||
const resultMap = buildResultMap(results);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.inset = "0";
|
||||
overlay.style.background = "rgba(5, 6, 10, 0.75)";
|
||||
overlay.style.zIndex = "10010";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.padding = "16px";
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.style.width = "min(640px, 92vw)";
|
||||
panel.style.maxHeight = "80vh";
|
||||
panel.style.overflow = "auto";
|
||||
panel.style.background = "rgba(18, 20, 26, 0.95)";
|
||||
panel.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.padding = "14px 16px";
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.gap = "10px";
|
||||
panel.style.color = "#eee";
|
||||
panel.style.fontSize = "12px";
|
||||
panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.4)";
|
||||
overlay.appendChild(panel);
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Fix Missing Models";
|
||||
title.style.fontWeight = "600";
|
||||
title.style.fontSize = "13px";
|
||||
title.style.letterSpacing = "0.4px";
|
||||
panel.appendChild(title);
|
||||
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.textContent = `Missing entries: ${missingList.length}`;
|
||||
subtitle.style.opacity = "0.8";
|
||||
subtitle.style.fontSize = "11px";
|
||||
panel.appendChild(subtitle);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.style.display = "flex";
|
||||
list.style.flexDirection = "column";
|
||||
list.style.gap = "8px";
|
||||
panel.appendChild(list);
|
||||
|
||||
const rows = [];
|
||||
for (const entry of missingList) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
const resultData = resultMap.get(key) || { candidates: [], exact_match_wrong_folder: null };
|
||||
const candidates = resultData.candidates || [];
|
||||
const wrongFolderMatch = resultData.exact_match_wrong_folder;
|
||||
const exactMatches = candidates.filter((c) => Number(c?.score || 0) === 100);
|
||||
const exactUnique = exactMatches.length === 1;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.style.display = "flex";
|
||||
row.style.flexDirection = "column";
|
||||
row.style.gap = "6px";
|
||||
row.style.padding = "8px";
|
||||
row.style.borderRadius = "8px";
|
||||
row.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||
row.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.style.display = "flex";
|
||||
label.style.alignItems = "center";
|
||||
label.style.gap = "6px";
|
||||
const titleText = document.createElement("div");
|
||||
const nodeLabel = entry?.node_title || `Node ${entry?.node_id ?? "?"}`;
|
||||
const widgetLabel = entry?.widget_name || "widget";
|
||||
titleText.textContent = `${nodeLabel} - ${widgetLabel}`;
|
||||
titleText.style.fontWeight = "600";
|
||||
const autoTag = document.createElement("span");
|
||||
if (exactUnique) {
|
||||
autoTag.textContent = "auto";
|
||||
autoTag.style.fontSize = "10px";
|
||||
autoTag.style.padding = "1px 6px";
|
||||
autoTag.style.borderRadius = "999px";
|
||||
autoTag.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
autoTag.style.border = "1px solid rgba(46, 136, 255, 0.45)";
|
||||
autoTag.style.color = "#cfe3ff";
|
||||
}
|
||||
label.appendChild(titleText);
|
||||
if (exactUnique) {
|
||||
label.appendChild(autoTag);
|
||||
}
|
||||
|
||||
const missingLine = document.createElement("div");
|
||||
missingLine.textContent = `Missing: ${missingValue}`;
|
||||
missingLine.style.opacity = "0.8";
|
||||
missingLine.style.fontSize = "11px";
|
||||
|
||||
// Show "Move to correct folder" button if exact match found in wrong folder
|
||||
let moveButton = null;
|
||||
if (wrongFolderMatch) {
|
||||
const moveContainer = document.createElement("div");
|
||||
moveContainer.style.display = "flex";
|
||||
moveContainer.style.alignItems = "center";
|
||||
moveContainer.style.gap = "8px";
|
||||
moveContainer.style.padding = "6px 8px";
|
||||
moveContainer.style.borderRadius = "8px";
|
||||
moveContainer.style.background = "rgba(255, 165, 0, 0.15)";
|
||||
moveContainer.style.border = "1px solid rgba(255, 165, 0, 0.3)";
|
||||
|
||||
const moveInfo = document.createElement("div");
|
||||
moveInfo.style.flex = "1";
|
||||
moveInfo.style.fontSize = "11px";
|
||||
moveInfo.style.color = "#ffb74d";
|
||||
moveInfo.textContent = `Found in ${wrongFolderMatch.kind}/ → Move to ${wrongFolderMatch.expected_kind}/`;
|
||||
|
||||
moveButton = document.createElement("button");
|
||||
moveButton.textContent = "Move";
|
||||
moveButton.className = "mjr-ps-btn";
|
||||
moveButton.style.padding = "4px 10px";
|
||||
moveButton.style.borderRadius = "6px";
|
||||
moveButton.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
moveButton.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
moveButton.style.color = "#ffcc80";
|
||||
moveButton.style.cursor = "pointer";
|
||||
moveButton.style.fontSize = "11px";
|
||||
moveButton.dataset.sourceKind = wrongFolderMatch.kind;
|
||||
moveButton.dataset.sourceRelpath = wrongFolderMatch.relpath;
|
||||
moveButton.dataset.targetKind = wrongFolderMatch.expected_kind;
|
||||
|
||||
moveContainer.appendChild(moveInfo);
|
||||
moveContainer.appendChild(moveButton);
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
row.appendChild(moveContainer);
|
||||
} else {
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
}
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.className = "mjr-ps-select";
|
||||
select.style.width = "100%";
|
||||
select.style.padding = "6px 8px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
select.style.color = "#f2f4f8";
|
||||
|
||||
const emptyOpt = document.createElement("option");
|
||||
emptyOpt.value = "";
|
||||
emptyOpt.textContent = candidates.length ? "-- Select match --" : "No candidates found";
|
||||
select.appendChild(emptyOpt);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(candidate?.relpath || "");
|
||||
opt.textContent = formatCandidateLabel(candidate);
|
||||
opt.title = String(candidate?.relpath || "");
|
||||
select.appendChild(opt);
|
||||
}
|
||||
if (exactUnique) {
|
||||
select.value = String(exactMatches[0]?.relpath || "");
|
||||
}
|
||||
if (!candidates.length) {
|
||||
select.disabled = true;
|
||||
select.style.opacity = "0.6";
|
||||
}
|
||||
|
||||
row.appendChild(select);
|
||||
list.appendChild(row);
|
||||
|
||||
rows.push({
|
||||
node_id: entry?.node_id,
|
||||
widget_name: entry?.widget_name,
|
||||
select,
|
||||
moveButton,
|
||||
});
|
||||
}
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "flex-end";
|
||||
actions.style.marginTop = "6px";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.className = "mjr-ps-btn";
|
||||
cancelBtn.style.padding = "6px 10px";
|
||||
cancelBtn.style.borderRadius = "8px";
|
||||
cancelBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
cancelBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
cancelBtn.style.color = "#eee";
|
||||
cancelBtn.style.cursor = "pointer";
|
||||
|
||||
const applyBtn = document.createElement("button");
|
||||
applyBtn.textContent = "Apply Fix";
|
||||
applyBtn.className = "mjr-ps-btn";
|
||||
applyBtn.style.padding = "6px 10px";
|
||||
applyBtn.style.borderRadius = "8px";
|
||||
applyBtn.style.border = "1px solid rgba(46, 136, 255, 0.65)";
|
||||
applyBtn.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
applyBtn.style.color = "#eee";
|
||||
applyBtn.style.cursor = "pointer";
|
||||
|
||||
actions.appendChild(cancelBtn);
|
||||
actions.appendChild(applyBtn);
|
||||
panel.appendChild(actions);
|
||||
|
||||
const close = (value) => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
if (overlay.parentNode) overlay.remove();
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (String(event.key || "").toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
close(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listeners to all move buttons
|
||||
for (const row of rows) {
|
||||
if (row.moveButton) {
|
||||
row.moveButton.addEventListener("click", async () => {
|
||||
const button = row.moveButton;
|
||||
const sourceKind = button.dataset.sourceKind;
|
||||
const sourceRelpath = button.dataset.sourceRelpath;
|
||||
const targetKind = button.dataset.targetKind;
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = "Moving...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/mjr_models/move_to_correct_folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
source_kind: sourceKind,
|
||||
source_relpath: sourceRelpath,
|
||||
target_kind: targetKind,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok) {
|
||||
button.textContent = "Moved!";
|
||||
button.style.background = "rgba(76, 175, 80, 0.25)";
|
||||
button.style.border = "1px solid rgba(76, 175, 80, 0.5)";
|
||||
button.style.color = "#a5d6a7";
|
||||
|
||||
// Auto-select the newly moved file in the dropdown
|
||||
if (row.select && data.target_relpath) {
|
||||
row.select.value = data.target_relpath;
|
||||
}
|
||||
|
||||
// Refresh the folder to show the new file
|
||||
setTimeout(() => {
|
||||
if (typeof app?.refreshComboInNodes === "function") {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
throw new Error(data.error || "Move failed");
|
||||
ensureStyles();
|
||||
const missingList = Array.isArray(missing) ? missing : [];
|
||||
const resultMap = buildResultMap(results);
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.inset = "0";
|
||||
overlay.style.background = "rgba(5, 6, 10, 0.75)";
|
||||
overlay.style.zIndex = "10010";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.padding = "16px";
|
||||
const panel = document.createElement("div");
|
||||
panel.style.width = "min(640px, 92vw)";
|
||||
panel.style.maxHeight = "80vh";
|
||||
panel.style.overflow = "auto";
|
||||
panel.style.background = "rgba(18, 20, 26, 0.95)";
|
||||
panel.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.padding = "14px 16px";
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.gap = "10px";
|
||||
panel.style.color = "#eee";
|
||||
panel.style.fontSize = "12px";
|
||||
panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.4)";
|
||||
overlay.appendChild(panel);
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Fix Missing Models";
|
||||
title.style.fontWeight = "600";
|
||||
title.style.fontSize = "13px";
|
||||
title.style.letterSpacing = "0.4px";
|
||||
panel.appendChild(title);
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.textContent = `Missing entries: ${missingList.length}`;
|
||||
subtitle.style.opacity = "0.8";
|
||||
subtitle.style.fontSize = "11px";
|
||||
panel.appendChild(subtitle);
|
||||
const list = document.createElement("div");
|
||||
list.style.display = "flex";
|
||||
list.style.flexDirection = "column";
|
||||
list.style.gap = "8px";
|
||||
panel.appendChild(list);
|
||||
const rows = [];
|
||||
for (const entry of missingList) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
const resultData = resultMap.get(key) || { candidates: [], exact_match_wrong_folder: null };
|
||||
const candidates = resultData.candidates || [];
|
||||
const wrongFolderMatch = resultData.exact_match_wrong_folder;
|
||||
const exactMatches = candidates.filter((c) => Number(c?.score || 0) === 100);
|
||||
const exactUnique = exactMatches.length === 1;
|
||||
const row = document.createElement("div");
|
||||
row.style.display = "flex";
|
||||
row.style.flexDirection = "column";
|
||||
row.style.gap = "6px";
|
||||
row.style.padding = "8px";
|
||||
row.style.borderRadius = "8px";
|
||||
row.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||
row.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
const label = document.createElement("div");
|
||||
label.style.display = "flex";
|
||||
label.style.alignItems = "center";
|
||||
label.style.gap = "6px";
|
||||
const titleText = document.createElement("div");
|
||||
const nodeLabel = entry?.node_title || `Node ${entry?.node_id ?? "?"}`;
|
||||
const widgetLabel = entry?.widget_name || "widget";
|
||||
titleText.textContent = `${nodeLabel} - ${widgetLabel}`;
|
||||
titleText.style.fontWeight = "600";
|
||||
const autoTag = document.createElement("span");
|
||||
if (exactUnique) {
|
||||
autoTag.textContent = "auto";
|
||||
autoTag.style.fontSize = "10px";
|
||||
autoTag.style.padding = "1px 6px";
|
||||
autoTag.style.borderRadius = "999px";
|
||||
autoTag.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
autoTag.style.border = "1px solid rgba(46, 136, 255, 0.45)";
|
||||
autoTag.style.color = "#cfe3ff";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[mjr] Failed to move model:", error);
|
||||
button.textContent = "Error";
|
||||
button.style.background = "rgba(244, 67, 54, 0.25)";
|
||||
button.style.border = "1px solid rgba(244, 67, 54, 0.5)";
|
||||
button.disabled = false;
|
||||
setTimeout(() => {
|
||||
button.textContent = "Move";
|
||||
button.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
button.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
}, 3000);
|
||||
}
|
||||
label.appendChild(titleText);
|
||||
if (exactUnique) {
|
||||
label.appendChild(autoTag);
|
||||
}
|
||||
const missingLine = document.createElement("div");
|
||||
missingLine.textContent = `Missing: ${missingValue}`;
|
||||
missingLine.style.opacity = "0.8";
|
||||
missingLine.style.fontSize = "11px";
|
||||
// Show "Move to correct folder" button if exact match found in wrong folder
|
||||
let moveButton = null;
|
||||
if (wrongFolderMatch) {
|
||||
const moveContainer = document.createElement("div");
|
||||
moveContainer.style.display = "flex";
|
||||
moveContainer.style.alignItems = "center";
|
||||
moveContainer.style.gap = "8px";
|
||||
moveContainer.style.padding = "6px 8px";
|
||||
moveContainer.style.borderRadius = "8px";
|
||||
moveContainer.style.background = "rgba(255, 165, 0, 0.15)";
|
||||
moveContainer.style.border = "1px solid rgba(255, 165, 0, 0.3)";
|
||||
const moveInfo = document.createElement("div");
|
||||
moveInfo.style.flex = "1";
|
||||
moveInfo.style.fontSize = "11px";
|
||||
moveInfo.style.color = "#ffb74d";
|
||||
moveInfo.textContent = `Found in ${wrongFolderMatch.kind}/ → Move to ${wrongFolderMatch.expected_kind}/`;
|
||||
moveButton = document.createElement("button");
|
||||
moveButton.textContent = "Move";
|
||||
moveButton.className = "mjr-ps-btn";
|
||||
moveButton.style.padding = "4px 10px";
|
||||
moveButton.style.borderRadius = "6px";
|
||||
moveButton.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
moveButton.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
moveButton.style.color = "#ffcc80";
|
||||
moveButton.style.cursor = "pointer";
|
||||
moveButton.style.fontSize = "11px";
|
||||
moveButton.dataset.sourceKind = wrongFolderMatch.kind;
|
||||
moveButton.dataset.sourceRelpath = wrongFolderMatch.relpath;
|
||||
moveButton.dataset.targetKind = wrongFolderMatch.expected_kind;
|
||||
moveContainer.appendChild(moveInfo);
|
||||
moveContainer.appendChild(moveButton);
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
row.appendChild(moveContainer);
|
||||
}
|
||||
else {
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
}
|
||||
const select = document.createElement("select");
|
||||
select.className = "mjr-ps-select";
|
||||
select.style.width = "100%";
|
||||
select.style.padding = "6px 8px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
select.style.color = "#f2f4f8";
|
||||
const emptyOpt = document.createElement("option");
|
||||
emptyOpt.value = "";
|
||||
emptyOpt.textContent = candidates.length ? "-- Select match --" : "No candidates found";
|
||||
select.appendChild(emptyOpt);
|
||||
for (const candidate of candidates) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(candidate?.relpath || "");
|
||||
opt.textContent = formatCandidateLabel(candidate);
|
||||
opt.title = String(candidate?.relpath || "");
|
||||
select.appendChild(opt);
|
||||
}
|
||||
if (exactUnique) {
|
||||
select.value = String(exactMatches[0]?.relpath || "");
|
||||
}
|
||||
if (!candidates.length) {
|
||||
select.disabled = true;
|
||||
select.style.opacity = "0.6";
|
||||
}
|
||||
row.appendChild(select);
|
||||
list.appendChild(row);
|
||||
rows.push({
|
||||
node_id: entry?.node_id,
|
||||
widget_name: entry?.widget_name,
|
||||
select,
|
||||
moveButton,
|
||||
});
|
||||
}
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "flex-end";
|
||||
actions.style.marginTop = "6px";
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.className = "mjr-ps-btn";
|
||||
cancelBtn.style.padding = "6px 10px";
|
||||
cancelBtn.style.borderRadius = "8px";
|
||||
cancelBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
cancelBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
cancelBtn.style.color = "#eee";
|
||||
cancelBtn.style.cursor = "pointer";
|
||||
const applyBtn = document.createElement("button");
|
||||
applyBtn.textContent = "Apply Fix";
|
||||
applyBtn.className = "mjr-ps-btn";
|
||||
applyBtn.style.padding = "6px 10px";
|
||||
applyBtn.style.borderRadius = "8px";
|
||||
applyBtn.style.border = "1px solid rgba(46, 136, 255, 0.65)";
|
||||
applyBtn.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
applyBtn.style.color = "#eee";
|
||||
applyBtn.style.cursor = "pointer";
|
||||
actions.appendChild(cancelBtn);
|
||||
actions.appendChild(applyBtn);
|
||||
panel.appendChild(actions);
|
||||
const close = (value) => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
if (overlay.parentNode)
|
||||
overlay.remove();
|
||||
resolve(value);
|
||||
};
|
||||
const onKeyDown = (event) => {
|
||||
if (String(event.key || "").toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
close(null);
|
||||
}
|
||||
};
|
||||
// Add event listeners to all move buttons
|
||||
for (const row of rows) {
|
||||
if (row.moveButton) {
|
||||
row.moveButton.addEventListener("click", async () => {
|
||||
const button = row.moveButton;
|
||||
const sourceKind = button.dataset.sourceKind;
|
||||
const sourceRelpath = button.dataset.sourceRelpath;
|
||||
const targetKind = button.dataset.targetKind;
|
||||
button.disabled = true;
|
||||
button.textContent = "Moving...";
|
||||
try {
|
||||
const response = await fetch("/mjr_models/move_to_correct_folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
source_kind: sourceKind,
|
||||
source_relpath: sourceRelpath,
|
||||
target_kind: targetKind,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ok) {
|
||||
button.textContent = "Moved!";
|
||||
button.style.background = "rgba(76, 175, 80, 0.25)";
|
||||
button.style.border = "1px solid rgba(76, 175, 80, 0.5)";
|
||||
button.style.color = "#a5d6a7";
|
||||
// Auto-select the newly moved file in the dropdown
|
||||
if (row.select && data.target_relpath) {
|
||||
row.select.value = data.target_relpath;
|
||||
}
|
||||
// Refresh the folder to show the new file
|
||||
setTimeout(() => {
|
||||
if (typeof app?.refreshComboInNodes === "function") {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
else {
|
||||
throw new Error(data.error || "Move failed");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("[mjr] Failed to move model:", error);
|
||||
button.textContent = "Error";
|
||||
button.style.background = "rgba(244, 67, 54, 0.25)";
|
||||
button.style.border = "1px solid rgba(244, 67, 54, 0.5)";
|
||||
button.disabled = false;
|
||||
setTimeout(() => {
|
||||
button.textContent = "Move";
|
||||
button.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
button.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const fixes = [];
|
||||
for (const row of rows) {
|
||||
const value = row.select.value;
|
||||
if (!value)
|
||||
continue;
|
||||
fixes.push({
|
||||
node_id: row.node_id,
|
||||
widget_name: row.widget_name,
|
||||
new_value: value,
|
||||
});
|
||||
}
|
||||
close(fixes);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const fixes = [];
|
||||
for (const row of rows) {
|
||||
const value = row.select.value;
|
||||
if (!value) continue;
|
||||
fixes.push({
|
||||
node_id: row.node_id,
|
||||
widget_name: row.widget_name,
|
||||
new_value: value,
|
||||
cancelBtn.addEventListener("click", () => close(null));
|
||||
overlay.addEventListener("click", (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
close(fixes);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => close(null));
|
||||
overlay.addEventListener("click", (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=ui_model_fixer_dialog.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export declare function titlePathJS(text: any): string;
|
||||
export declare function token3Tag(raw: any, upper: any): string;
|
||||
export declare function yymmddJS(): string;
|
||||
export declare function safeRel(rel: any): string;
|
||||
export declare function joinRel(a: any, b: any): string;
|
||||
export declare function mediaDir(media: any): "02_OUT/VIDEOS" | "02_OUT/IMAGES" | "02_OUT/OTHER";
|
||||
export declare function makeKindToken(kind: any): "ASSET" | "SHOT" | "MISC";
|
||||
export declare function resolveTemplatePreview(template: any, tokens: any): any;
|
||||
+110
-113
@@ -1,136 +1,133 @@
|
||||
export function titlePathJS(text) {
|
||||
if (text == null) return "";
|
||||
let t = String(text).trim();
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/[\\/]/g, " ");
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean);
|
||||
const titled = tokens.map((tok) => {
|
||||
if (tok.length <= 1) return tok.toUpperCase();
|
||||
return tok[0].toUpperCase() + tok.slice(1).toLowerCase();
|
||||
});
|
||||
return titled.join("_") || "Project";
|
||||
if (text == null)
|
||||
return "";
|
||||
let t = String(text).trim();
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/[\\/]/g, " ");
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean);
|
||||
const titled = tokens.map((tok) => {
|
||||
if (tok.length <= 1)
|
||||
return tok.toUpperCase();
|
||||
return tok[0].toUpperCase() + tok.slice(1).toLowerCase();
|
||||
});
|
||||
return titled.join("_") || "Project";
|
||||
}
|
||||
|
||||
export function token3Tag(raw, upper) {
|
||||
if (raw == null) return "";
|
||||
let t = String(raw).trim();
|
||||
if (!t) return "";
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/\\/g, "/").split("/").pop();
|
||||
const lastDot = t.lastIndexOf(".");
|
||||
if (lastDot > 0) {
|
||||
t = t.slice(0, lastDot);
|
||||
}
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean).slice(0, 3);
|
||||
if (!tokens.length) return "";
|
||||
if (upper) {
|
||||
return tokens.join("_").toUpperCase();
|
||||
}
|
||||
return tokens
|
||||
.map((tok) =>
|
||||
tok.length <= 1 ? tok.toUpperCase() : tok[0].toUpperCase() + tok.slice(1).toLowerCase()
|
||||
)
|
||||
.join("_");
|
||||
if (raw == null)
|
||||
return "";
|
||||
let t = String(raw).trim();
|
||||
if (!t)
|
||||
return "";
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/\\/g, "/").split("/").pop();
|
||||
const lastDot = t.lastIndexOf(".");
|
||||
if (lastDot > 0) {
|
||||
t = t.slice(0, lastDot);
|
||||
}
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean).slice(0, 3);
|
||||
if (!tokens.length)
|
||||
return "";
|
||||
if (upper) {
|
||||
return tokens.join("_").toUpperCase();
|
||||
}
|
||||
return tokens
|
||||
.map((tok) => tok.length <= 1 ? tok.toUpperCase() : tok[0].toUpperCase() + tok.slice(1).toLowerCase())
|
||||
.join("_");
|
||||
}
|
||||
|
||||
export function yymmddJS() {
|
||||
const d = new Date();
|
||||
const yy = String(d.getFullYear()).slice(-2);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${yy}${mm}${dd}`;
|
||||
const d = new Date();
|
||||
const yy = String(d.getFullYear()).slice(-2);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${yy}${mm}${dd}`;
|
||||
}
|
||||
|
||||
export function safeRel(rel) {
|
||||
if (rel == null) return "";
|
||||
let r = String(rel).replace(/\\/g, "/").trim();
|
||||
r = r.replace(/^output\//i, "");
|
||||
r = r.replace(/^\/+/, "");
|
||||
r = r
|
||||
.split("/")
|
||||
.filter((p) => p && p !== "." && p !== "..")
|
||||
.join("/");
|
||||
return r;
|
||||
if (rel == null)
|
||||
return "";
|
||||
let r = String(rel).replace(/\\/g, "/").trim();
|
||||
r = r.replace(/^output\//i, "");
|
||||
r = r.replace(/^\/+/, "");
|
||||
r = r
|
||||
.split("/")
|
||||
.filter((p) => p && p !== "." && p !== "..")
|
||||
.join("/");
|
||||
return r;
|
||||
}
|
||||
|
||||
export function joinRel(a, b) {
|
||||
const left = safeRel(a);
|
||||
const right = safeRel(b);
|
||||
if (!left) return right;
|
||||
if (!right) return left;
|
||||
return `${left.replace(/\/+$/, "")}/${right.replace(/^\/+/, "")}`;
|
||||
const left = safeRel(a);
|
||||
const right = safeRel(b);
|
||||
if (!left)
|
||||
return right;
|
||||
if (!right)
|
||||
return left;
|
||||
return `${left.replace(/\/+$/, "")}/${right.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
export function mediaDir(media) {
|
||||
const m = String(media || "").toLowerCase();
|
||||
if (m === "videos") return "02_OUT/VIDEOS";
|
||||
if (m === "images") return "02_OUT/IMAGES";
|
||||
return "02_OUT/OTHER";
|
||||
const m = String(media || "").toLowerCase();
|
||||
if (m === "videos")
|
||||
return "02_OUT/VIDEOS";
|
||||
if (m === "images")
|
||||
return "02_OUT/IMAGES";
|
||||
return "02_OUT/OTHER";
|
||||
}
|
||||
|
||||
export function makeKindToken(kind) {
|
||||
const k = String(kind || "").toLowerCase();
|
||||
if (k === "asset") return "ASSET";
|
||||
if (k === "shot") return "SHOT";
|
||||
return "MISC";
|
||||
const k = String(kind || "").toLowerCase();
|
||||
if (k === "asset")
|
||||
return "ASSET";
|
||||
if (k === "shot")
|
||||
return "SHOT";
|
||||
return "MISC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a path is relative and safe.
|
||||
*/
|
||||
function validateRelativePath(path, context) {
|
||||
if (path.startsWith("/") || path.startsWith("\\")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes(":")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes("..")) {
|
||||
throw new Error(`${context} contains '..'`);
|
||||
}
|
||||
if (path.startsWith("/") || path.startsWith("\\")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes(":")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes("..")) {
|
||||
throw new Error(`${context} contains '..'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTemplatePreview(template, tokens) {
|
||||
if (!template) return "";
|
||||
let out = template.replace(/\\/g, "/").trim();
|
||||
if (!out) return "";
|
||||
|
||||
// Validate template before processing
|
||||
validateRelativePath(out, "template");
|
||||
|
||||
if (!out.includes("{BASE}")) {
|
||||
throw new Error("template must include {BASE}");
|
||||
}
|
||||
|
||||
// Replace tokens (use split/join to handle multiple occurrences safely)
|
||||
for (const [key, value] of Object.entries(tokens)) {
|
||||
const safeValue = String(value || "");
|
||||
// Prevent token values from containing other token patterns
|
||||
if (safeValue.includes("{") || safeValue.includes("}")) {
|
||||
throw new Error(`token value for {${key}} contains template markers`);
|
||||
if (!template)
|
||||
return "";
|
||||
let out = template.replace(/\\/g, "/").trim();
|
||||
if (!out)
|
||||
return "";
|
||||
// Validate template before processing
|
||||
validateRelativePath(out, "template");
|
||||
if (!out.includes("{BASE}")) {
|
||||
throw new Error("template must include {BASE}");
|
||||
}
|
||||
out = out.split(`{${key}}`).join(safeValue);
|
||||
}
|
||||
|
||||
// Normalize slashes
|
||||
out = out.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
|
||||
// Validate resolved path
|
||||
validateRelativePath(out, "resolved path");
|
||||
|
||||
const parts = out.split("/").filter(Boolean);
|
||||
|
||||
// Verify base constraint
|
||||
const base = String(tokens?.BASE || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (base) {
|
||||
if (!(out === base || out.startsWith(`${base}/`))) {
|
||||
throw new Error("template must stay under {BASE}");
|
||||
// Replace tokens (use split/join to handle multiple occurrences safely)
|
||||
for (const [key, value] of Object.entries(tokens)) {
|
||||
const safeValue = String(value || "");
|
||||
// Prevent token values from containing other token patterns
|
||||
if (safeValue.includes("{") || safeValue.includes("}")) {
|
||||
throw new Error(`token value for {${key}} contains template markers`);
|
||||
}
|
||||
out = out.split(`{${key}}`).join(safeValue);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("/");
|
||||
// Normalize slashes
|
||||
out = out.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
// Validate resolved path
|
||||
validateRelativePath(out, "resolved path");
|
||||
const parts = out.split("/").filter(Boolean);
|
||||
// Verify base constraint
|
||||
const base = String(tokens?.BASE || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (base) {
|
||||
if (!(out === base || out.startsWith(`${base}/`))) {
|
||||
throw new Error("template must stay under {BASE}");
|
||||
}
|
||||
}
|
||||
return parts.join("/");
|
||||
}
|
||||
//# sourceMappingURL=utils.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export declare function extractNoteTexts(workflowJson: any): unknown[];
|
||||
export declare function parseRecipesFromNoteText(text: any): Map<any, any>;
|
||||
export declare function collectRecipesFromWorkflowNotes(workflowJson: any): Map<any, any>;
|
||||
+178
-180
@@ -2,212 +2,210 @@ const NOTE_TYPE_RE = /markdown|note/i;
|
||||
const NOTE_TITLE_RE = /note/i;
|
||||
const ALLOWED_EXTENSIONS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin"];
|
||||
const SECTION_KINDS = new Set([
|
||||
"text_encoders",
|
||||
"clip_vision",
|
||||
"loras",
|
||||
"diffusion_models",
|
||||
"vae",
|
||||
"text_encoders",
|
||||
"clip_vision",
|
||||
"loras",
|
||||
"diffusion_models",
|
||||
"vae",
|
||||
]);
|
||||
|
||||
function normalizeSectionLine(line) {
|
||||
let t = String(line || "").trim().toLowerCase();
|
||||
if (!t) return "";
|
||||
t = t.replace(/^#+\s*/, "");
|
||||
t = t.replace(/:\s*$/, "");
|
||||
return t.trim();
|
||||
let t = String(line || "").trim().toLowerCase();
|
||||
if (!t)
|
||||
return "";
|
||||
t = t.replace(/^#+\s*/, "");
|
||||
t = t.replace(/:\s*$/, "");
|
||||
return t.trim();
|
||||
}
|
||||
|
||||
function normalizeUrl(url) {
|
||||
let u = String(url || "").trim();
|
||||
if (!u) return "";
|
||||
u = u.replace(/\/blob\//i, "/resolve/");
|
||||
return u;
|
||||
let u = String(url || "").trim();
|
||||
if (!u)
|
||||
return "";
|
||||
u = u.replace(/\/blob\//i, "/resolve/");
|
||||
return u;
|
||||
}
|
||||
|
||||
function basenameFromUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const path = parsed.pathname || "";
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const base = parts.length ? parts[parts.length - 1] : "";
|
||||
return base;
|
||||
} catch (_) {
|
||||
const clean = String(url || "").split("?")[0].split("#")[0];
|
||||
const parts = clean.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const path = parsed.pathname || "";
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const base = parts.length ? parts[parts.length - 1] : "";
|
||||
return base;
|
||||
}
|
||||
catch (_) {
|
||||
const clean = String(url || "").split("?")[0].split("#")[0];
|
||||
const parts = clean.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
}
|
||||
}
|
||||
|
||||
function hasAllowedExtension(filename) {
|
||||
const lower = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||
const lower = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
function inferKindFromUrl(url) {
|
||||
const lower = String(url || "").toLowerCase();
|
||||
|
||||
// Check URL path for kind indicators
|
||||
if (lower.includes("/text_encoders/") || lower.includes("text_encoder")) return "text_encoders";
|
||||
if (lower.includes("/diffusion_models/") || lower.includes("diffusion_model")) return "diffusion_models";
|
||||
if (lower.includes("/clip_vision/")) return "clip_vision";
|
||||
if (lower.includes("/loras/") || lower.includes("/lora/")) return "loras";
|
||||
if (lower.includes("/vae/")) return "vae";
|
||||
if (lower.includes("/controlnet/")) return "controlnet";
|
||||
if (lower.includes("/upscale_models/") || lower.includes("/upscale/")) return "upscale_models";
|
||||
if (lower.includes("/clip/")) return "clip";
|
||||
if (lower.includes("/unet/")) return "unet";
|
||||
if (lower.includes("/embeddings/") || lower.includes("/embedding/")) return "embeddings";
|
||||
if (lower.includes("/checkpoints/") || lower.includes("/checkpoint/")) return "checkpoints";
|
||||
|
||||
return "unknown";
|
||||
const lower = String(url || "").toLowerCase();
|
||||
// Check URL path for kind indicators
|
||||
if (lower.includes("/text_encoders/") || lower.includes("text_encoder"))
|
||||
return "text_encoders";
|
||||
if (lower.includes("/diffusion_models/") || lower.includes("diffusion_model"))
|
||||
return "diffusion_models";
|
||||
if (lower.includes("/clip_vision/"))
|
||||
return "clip_vision";
|
||||
if (lower.includes("/loras/") || lower.includes("/lora/"))
|
||||
return "loras";
|
||||
if (lower.includes("/vae/"))
|
||||
return "vae";
|
||||
if (lower.includes("/controlnet/"))
|
||||
return "controlnet";
|
||||
if (lower.includes("/upscale_models/") || lower.includes("/upscale/"))
|
||||
return "upscale_models";
|
||||
if (lower.includes("/clip/"))
|
||||
return "clip";
|
||||
if (lower.includes("/unet/"))
|
||||
return "unet";
|
||||
if (lower.includes("/embeddings/") || lower.includes("/embedding/"))
|
||||
return "embeddings";
|
||||
if (lower.includes("/checkpoints/") || lower.includes("/checkpoint/"))
|
||||
return "checkpoints";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getNoteTextFromNode(node) {
|
||||
const props = node?.properties || {};
|
||||
const candidates = [
|
||||
props?.text,
|
||||
props?.value,
|
||||
props?.markdown,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === "string" && candidate.trim().length > 20) {
|
||||
return candidate;
|
||||
const props = node?.properties || {};
|
||||
const candidates = [
|
||||
props?.text,
|
||||
props?.value,
|
||||
props?.markdown,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === "string" && candidate.trim().length > 20) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const widgetsValues = node?.widgets_values;
|
||||
if (Array.isArray(widgetsValues)) {
|
||||
const parts = widgetsValues.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20) return joined;
|
||||
}
|
||||
|
||||
const widgets = node?.widgets;
|
||||
if (Array.isArray(widgets)) {
|
||||
const parts = widgets
|
||||
.map((w) => w?.value)
|
||||
.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20) return joined;
|
||||
}
|
||||
return "";
|
||||
const widgetsValues = node?.widgets_values;
|
||||
if (Array.isArray(widgetsValues)) {
|
||||
const parts = widgetsValues.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20)
|
||||
return joined;
|
||||
}
|
||||
const widgets = node?.widgets;
|
||||
if (Array.isArray(widgets)) {
|
||||
const parts = widgets
|
||||
.map((w) => w?.value)
|
||||
.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20)
|
||||
return joined;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isNoteLikeNode(node) {
|
||||
const type = String(node?.type || node?.class_type || "").trim();
|
||||
const title = String(node?.title || "").trim();
|
||||
return NOTE_TYPE_RE.test(type) || NOTE_TITLE_RE.test(title);
|
||||
const type = String(node?.type || node?.class_type || "").trim();
|
||||
const title = String(node?.title || "").trim();
|
||||
return NOTE_TYPE_RE.test(type) || NOTE_TITLE_RE.test(title);
|
||||
}
|
||||
|
||||
function collectGraphsFromNode(node) {
|
||||
return [
|
||||
node?.subgraph,
|
||||
node?._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node?.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
].filter(Boolean);
|
||||
return [
|
||||
node?.subgraph,
|
||||
node?._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node?.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function walkGraph(graph, visitor, visited) {
|
||||
if (!graph || !Array.isArray(graph.nodes)) return;
|
||||
if (visited.has(graph)) return;
|
||||
visited.add(graph);
|
||||
for (const node of graph.nodes) {
|
||||
visitor(node);
|
||||
const children = collectGraphsFromNode(node);
|
||||
for (const child of children) {
|
||||
if (Array.isArray(child?.nodes)) {
|
||||
walkGraph(child, visitor, visited);
|
||||
}
|
||||
if (!graph || !Array.isArray(graph.nodes))
|
||||
return;
|
||||
if (visited.has(graph))
|
||||
return;
|
||||
visited.add(graph);
|
||||
for (const node of graph.nodes) {
|
||||
visitor(node);
|
||||
const children = collectGraphsFromNode(node);
|
||||
for (const child of children) {
|
||||
if (Array.isArray(child?.nodes)) {
|
||||
walkGraph(child, visitor, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractNoteTexts(workflowJson) {
|
||||
if (!workflowJson || !Array.isArray(workflowJson.nodes)) return [];
|
||||
const texts = new Set();
|
||||
const visited = new WeakSet();
|
||||
const graph = { nodes: workflowJson.nodes };
|
||||
|
||||
walkGraph(
|
||||
graph,
|
||||
(node) => {
|
||||
if (!isNoteLikeNode(node)) return;
|
||||
const text = getNoteTextFromNode(node);
|
||||
if (text && text.trim().length > 20) {
|
||||
texts.add(text.trim());
|
||||
}
|
||||
},
|
||||
visited
|
||||
);
|
||||
|
||||
return Array.from(texts);
|
||||
if (!workflowJson || !Array.isArray(workflowJson.nodes))
|
||||
return [];
|
||||
const texts = new Set();
|
||||
const visited = new WeakSet();
|
||||
const graph = { nodes: workflowJson.nodes };
|
||||
walkGraph(graph, (node) => {
|
||||
if (!isNoteLikeNode(node))
|
||||
return;
|
||||
const text = getNoteTextFromNode(node);
|
||||
if (text && text.trim().length > 20) {
|
||||
texts.add(text.trim());
|
||||
}
|
||||
}, visited);
|
||||
return Array.from(texts);
|
||||
}
|
||||
|
||||
export function parseRecipesFromNoteText(text) {
|
||||
const recipes = new Map();
|
||||
const lines = String(text || "").split(/\r?\n/);
|
||||
let currentKind = "unknown";
|
||||
|
||||
for (const line of lines) {
|
||||
const normalized = normalizeSectionLine(line);
|
||||
if (SECTION_KINDS.has(normalized)) {
|
||||
currentKind = normalized;
|
||||
continue;
|
||||
const recipes = new Map();
|
||||
const lines = String(text || "").split(/\r?\n/);
|
||||
let currentKind = "unknown";
|
||||
for (const line of lines) {
|
||||
const normalized = normalizeSectionLine(line);
|
||||
if (SECTION_KINDS.has(normalized)) {
|
||||
currentKind = normalized;
|
||||
continue;
|
||||
}
|
||||
const urls = new Set();
|
||||
const mdRegex = /\[[^\]]+\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
let match = null;
|
||||
while ((match = mdRegex.exec(line)) !== null) {
|
||||
if (match[1])
|
||||
urls.add(match[1]);
|
||||
}
|
||||
const rawRegex = /(https?:\/\/[^\s]+)/g;
|
||||
while ((match = rawRegex.exec(line)) !== null) {
|
||||
if (match[1])
|
||||
urls.add(match[1]);
|
||||
}
|
||||
for (const url of urls) {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
if (!normalizedUrl)
|
||||
continue;
|
||||
const filename = basenameFromUrl(normalizedUrl);
|
||||
if (!filename || !hasAllowedExtension(filename))
|
||||
continue;
|
||||
if (recipes.has(filename)) {
|
||||
console.debug("[mjr] workflow note recipe duplicate", filename);
|
||||
continue;
|
||||
}
|
||||
// Use section kind if available, otherwise infer from URL
|
||||
let finalKind = currentKind;
|
||||
if (!finalKind || finalKind === "unknown") {
|
||||
finalKind = inferKindFromUrl(normalizedUrl);
|
||||
}
|
||||
recipes.set(filename, {
|
||||
key: filename,
|
||||
kind: finalKind,
|
||||
url: normalizedUrl,
|
||||
filename,
|
||||
source: "workflow_note",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const urls = new Set();
|
||||
const mdRegex = /\[[^\]]+\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
let match = null;
|
||||
while ((match = mdRegex.exec(line)) !== null) {
|
||||
if (match[1]) urls.add(match[1]);
|
||||
}
|
||||
const rawRegex = /(https?:\/\/[^\s]+)/g;
|
||||
while ((match = rawRegex.exec(line)) !== null) {
|
||||
if (match[1]) urls.add(match[1]);
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
if (!normalizedUrl) continue;
|
||||
const filename = basenameFromUrl(normalizedUrl);
|
||||
if (!filename || !hasAllowedExtension(filename)) continue;
|
||||
if (recipes.has(filename)) {
|
||||
console.debug("[mjr] workflow note recipe duplicate", filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use section kind if available, otherwise infer from URL
|
||||
let finalKind = currentKind;
|
||||
if (!finalKind || finalKind === "unknown") {
|
||||
finalKind = inferKindFromUrl(normalizedUrl);
|
||||
}
|
||||
|
||||
recipes.set(filename, {
|
||||
key: filename,
|
||||
kind: finalKind,
|
||||
url: normalizedUrl,
|
||||
filename,
|
||||
source: "workflow_note",
|
||||
});
|
||||
}
|
||||
}
|
||||
return recipes;
|
||||
return recipes;
|
||||
}
|
||||
|
||||
export function collectRecipesFromWorkflowNotes(workflowJson) {
|
||||
const texts = extractNoteTexts(workflowJson);
|
||||
const merged = new Map();
|
||||
for (const text of texts) {
|
||||
const parsed = parseRecipesFromNoteText(text);
|
||||
for (const [key, recipe] of parsed.entries()) {
|
||||
if (!merged.has(key)) {
|
||||
merged.set(key, recipe);
|
||||
}
|
||||
const texts = extractNoteTexts(workflowJson);
|
||||
const merged = new Map();
|
||||
for (const text of texts) {
|
||||
const parsed = parseRecipesFromNoteText(text);
|
||||
for (const [key, recipe] of parsed.entries()) {
|
||||
if (!merged.has(key)) {
|
||||
merged.set(key, recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
return merged;
|
||||
}
|
||||
//# sourceMappingURL=workflow_note_recipes.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
export declare const DEFAULT_STATE: {
|
||||
projectName: string;
|
||||
workflowName: string;
|
||||
workflowAsset: string;
|
||||
workflowLastBase: string;
|
||||
workflowNextSuffix: number;
|
||||
workflowPanelOpen: boolean;
|
||||
projectId: string;
|
||||
projectFolder: string;
|
||||
projectExists: any;
|
||||
autoPatch: boolean;
|
||||
autoPatchNewNodes: boolean;
|
||||
autoSwitchTrusted: boolean;
|
||||
autoHookSave: boolean;
|
||||
kind: string;
|
||||
media: string;
|
||||
name: string;
|
||||
modelSelection: string;
|
||||
detectedModelTag: string;
|
||||
useCustomModel: boolean;
|
||||
customModelText: string;
|
||||
modelUpper: boolean;
|
||||
pendingProjectName: string;
|
||||
pendingSelectProjectId: string;
|
||||
lastRelDir: string;
|
||||
lastPrefix: string;
|
||||
pathTemplate: string;
|
||||
showMoreModels: boolean;
|
||||
lastError: string;
|
||||
detected: any;
|
||||
workflowHasSignature: boolean;
|
||||
workflowChecked: boolean;
|
||||
isGraphLoading: boolean;
|
||||
graphLoadToken: number;
|
||||
graphIsEmpty: boolean;
|
||||
};
|
||||
export declare let runtimeState: any;
|
||||
export declare function setRuntimeState(state: any): void;
|
||||
export declare function createRuntimeState(overrides: any): any;
|
||||
export declare function loadState(): any;
|
||||
export declare function saveState(state: any): void;
|
||||
+78
-80
@@ -1,92 +1,90 @@
|
||||
const STORAGE_KEY = "mjr_project_settings_state";
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
projectName: "",
|
||||
workflowName: "",
|
||||
workflowAsset: "",
|
||||
workflowLastBase: "",
|
||||
workflowNextSuffix: 1,
|
||||
workflowPanelOpen: false,
|
||||
projectId: "",
|
||||
projectFolder: "",
|
||||
projectExists: null,
|
||||
autoPatch: true,
|
||||
autoPatchNewNodes: true,
|
||||
autoSwitchTrusted: false,
|
||||
autoHookSave: true,
|
||||
kind: "asset",
|
||||
media: "images",
|
||||
name: "",
|
||||
modelSelection: "",
|
||||
detectedModelTag: "",
|
||||
useCustomModel: false,
|
||||
customModelText: "",
|
||||
modelUpper: false,
|
||||
pendingProjectName: "",
|
||||
pendingSelectProjectId: "",
|
||||
lastRelDir: "",
|
||||
lastPrefix: "",
|
||||
pathTemplate: "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}",
|
||||
showMoreModels: false,
|
||||
lastError: "",
|
||||
detected: null,
|
||||
workflowHasSignature: false,
|
||||
workflowChecked: false,
|
||||
isGraphLoading: false,
|
||||
graphLoadToken: 0,
|
||||
graphIsEmpty: false,
|
||||
projectName: "",
|
||||
workflowName: "",
|
||||
workflowAsset: "",
|
||||
workflowLastBase: "",
|
||||
workflowNextSuffix: 1,
|
||||
workflowPanelOpen: false,
|
||||
projectId: "",
|
||||
projectFolder: "",
|
||||
projectExists: null,
|
||||
autoPatch: true,
|
||||
autoPatchNewNodes: true,
|
||||
autoSwitchTrusted: false,
|
||||
autoHookSave: true,
|
||||
kind: "asset",
|
||||
media: "images",
|
||||
name: "",
|
||||
modelSelection: "",
|
||||
detectedModelTag: "",
|
||||
useCustomModel: false,
|
||||
customModelText: "",
|
||||
modelUpper: false,
|
||||
pendingProjectName: "",
|
||||
pendingSelectProjectId: "",
|
||||
lastRelDir: "",
|
||||
lastPrefix: "",
|
||||
pathTemplate: "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}",
|
||||
showMoreModels: false,
|
||||
lastError: "",
|
||||
detected: null,
|
||||
workflowHasSignature: false,
|
||||
workflowChecked: false,
|
||||
isGraphLoading: false,
|
||||
graphLoadToken: 0,
|
||||
graphIsEmpty: false,
|
||||
};
|
||||
|
||||
export let runtimeState = null;
|
||||
|
||||
export function setRuntimeState(state) {
|
||||
runtimeState = state;
|
||||
runtimeState = state;
|
||||
}
|
||||
|
||||
export function createRuntimeState(overrides) {
|
||||
return Object.assign({}, DEFAULT_STATE, overrides || {});
|
||||
return Object.assign({}, DEFAULT_STATE, overrides || {});
|
||||
}
|
||||
|
||||
export function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw)
|
||||
return null;
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveState(state) {
|
||||
const data = {
|
||||
projectName: state.projectName,
|
||||
workflowName: state.workflowName,
|
||||
workflowAsset: state.workflowAsset,
|
||||
workflowLastBase: state.workflowLastBase,
|
||||
workflowNextSuffix: state.workflowNextSuffix,
|
||||
workflowPanelOpen: state.workflowPanelOpen,
|
||||
projectId: state.projectId,
|
||||
projectFolder: state.projectFolder,
|
||||
autoPatch: state.autoPatch,
|
||||
autoPatchNewNodes: state.autoPatchNewNodes,
|
||||
autoSwitchTrusted: state.autoSwitchTrusted,
|
||||
autoHookSave: state.autoHookSave,
|
||||
kind: state.kind,
|
||||
media: state.media,
|
||||
name: state.name,
|
||||
modelSelection: state.modelSelection,
|
||||
detectedModelTag: state.detectedModelTag,
|
||||
useCustomModel: state.useCustomModel,
|
||||
customModelText: state.customModelText,
|
||||
modelUpper: state.modelUpper,
|
||||
pendingProjectName: state.pendingProjectName,
|
||||
pendingSelectProjectId: state.pendingSelectProjectId,
|
||||
lastRelDir: state.lastRelDir,
|
||||
lastPrefix: state.lastPrefix,
|
||||
pathTemplate: state.pathTemplate,
|
||||
showMoreModels: state.showMoreModels,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (_) {}
|
||||
const data = {
|
||||
projectName: state.projectName,
|
||||
workflowName: state.workflowName,
|
||||
workflowAsset: state.workflowAsset,
|
||||
workflowLastBase: state.workflowLastBase,
|
||||
workflowNextSuffix: state.workflowNextSuffix,
|
||||
workflowPanelOpen: state.workflowPanelOpen,
|
||||
projectId: state.projectId,
|
||||
projectFolder: state.projectFolder,
|
||||
autoPatch: state.autoPatch,
|
||||
autoPatchNewNodes: state.autoPatchNewNodes,
|
||||
autoSwitchTrusted: state.autoSwitchTrusted,
|
||||
autoHookSave: state.autoHookSave,
|
||||
kind: state.kind,
|
||||
media: state.media,
|
||||
name: state.name,
|
||||
modelSelection: state.modelSelection,
|
||||
detectedModelTag: state.detectedModelTag,
|
||||
useCustomModel: state.useCustomModel,
|
||||
customModelText: state.customModelText,
|
||||
modelUpper: state.modelUpper,
|
||||
pendingProjectName: state.pendingProjectName,
|
||||
pendingSelectProjectId: state.pendingSelectProjectId,
|
||||
lastRelDir: state.lastRelDir,
|
||||
lastPrefix: state.lastPrefix,
|
||||
pathTemplate: state.pathTemplate,
|
||||
showMoreModels: state.showMoreModels,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
catch (_) { }
|
||||
}
|
||||
//# sourceMappingURL=state_manager.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"state_manager.js","sourceRoot":"","sources":["../src/state_manager.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function buildPanel(el: any, state: any, actions: any): void;
|
||||
+2123
-2334
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "comfyui-majoor-projectsettings",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "comfyui-majoor-projectsettings",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "comfyui-majoor-projectsettings",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
/**
|
||||
* Majoor Project Settings (standalone)
|
||||
* - Sidebar UI when available (fallback floating panel)
|
||||
* - No nodes, no polling
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { joinRel, mediaDir, token3Tag, yymmddJS } from "./mjr/utils.js";
|
||||
import { toast } from "./mjr/toast.js";
|
||||
import { fetchJSON, saveWorkflow, tryResolveProjectId } from "./mjr/api.js";
|
||||
import { psConfirm, psPrompt } from "./mjr/dialog.js";
|
||||
import {
|
||||
detectModelFromGraph,
|
||||
getSerializedWorkflow,
|
||||
inferProjectFolderFromGraph,
|
||||
readGraphSignature,
|
||||
} from "./mjr/graph.js";
|
||||
import {
|
||||
PATH_WIDGETS,
|
||||
alreadyProjectPathed,
|
||||
detectNodeMedia,
|
||||
isSaveLikeNode,
|
||||
loadConfig,
|
||||
patchSaveNodes,
|
||||
patchSingleNode,
|
||||
stampGraphProjectSignature,
|
||||
} from "./mjr/patch.js";
|
||||
import { buildPanel } from "./ui_components.js";
|
||||
import {
|
||||
createRuntimeState,
|
||||
loadState,
|
||||
saveState,
|
||||
runtimeState,
|
||||
setRuntimeState,
|
||||
} from "./state_manager.js";
|
||||
import { scanMissingModelsWithStats } from "./mjr/model_fixer.js";
|
||||
|
||||
let workflowShortcutRegistered = false;
|
||||
|
||||
const NODE_PATCH_RETRY_DELAY_MS = 60;
|
||||
const NODE_PATCH_MAX_RETRIES = 1;
|
||||
const SETTINGS_CATEGORY = "MajoorPS";
|
||||
const DOWNLOAD_MAX_GB_SETTING = "mjr_project.download_max_gb";
|
||||
const DEFAULT_DOWNLOAD_MAX_GB = 50;
|
||||
const AUTO_FIX_AFTER_DOWNLOAD_SETTING = "mjr_project.auto_fix_after_download";
|
||||
const AUTO_OPEN_PANEL_ON_MISSING_SETTING = "mjr_project.auto_open_panel_on_missing";
|
||||
const AUTO_SCAN_MISSING_ON_OPEN_SETTING = "mjr_project.auto_scan_missing_on_open";
|
||||
const AUTO_REMEMBER_NOTE_SOURCES_SETTING = "mjr_project.auto_remember_note_sources";
|
||||
const NO_PROJECT_STATUS_BLACK_SETTING = "mjr_project.status_black_no_project";
|
||||
const DEFAULT_AUTO_FIX_AFTER_DOWNLOAD = true;
|
||||
const DEFAULT_AUTO_OPEN_PANEL = true;
|
||||
const DEFAULT_AUTO_SCAN_MISSING_ON_OPEN = true;
|
||||
const DEFAULT_AUTO_REMEMBER_NOTE_SOURCES = true;
|
||||
const DEFAULT_NO_PROJECT_STATUS_BLACK = false;
|
||||
const GRAPH_EMPTY_POLL_INTERVAL_MS = 900;
|
||||
let graphEmptyWatcherId = null;
|
||||
|
||||
function _extractYYMMDDFromPrefix(prefix) {
|
||||
const m = String(prefix || "").match(/^(\d{6})_/);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
|
||||
function _extractYYMMDDFromRelDir(relDir) {
|
||||
const m = String(relDir || "").replace(/\\/g, "/").match(/(?:^|\/)(\d{6})(?:\/|$)/);
|
||||
return m ? m[1] : "";
|
||||
}
|
||||
|
||||
function _replaceDateSegment(relDir, oldDate, newDate) {
|
||||
if (!oldDate || !newDate || oldDate === newDate) return relDir;
|
||||
const s = String(relDir || "").replace(/\\/g, "/");
|
||||
const re = new RegExp(`(^|/)${oldDate}(/|$)`, "g");
|
||||
return s.replace(re, `$1${newDate}$2`);
|
||||
}
|
||||
|
||||
function _rollOutputDateIfNeeded(state) {
|
||||
if (!state) return false;
|
||||
const today = yymmddJS();
|
||||
const oldDate = _extractYYMMDDFromPrefix(state.lastPrefix) || _extractYYMMDDFromRelDir(state.lastRelDir);
|
||||
if (!oldDate || oldDate === today) return false;
|
||||
|
||||
state.lastRelDir = _replaceDateSegment(state.lastRelDir, oldDate, today);
|
||||
if (String(state.lastPrefix || "").startsWith(`${oldDate}_`)) {
|
||||
state.lastPrefix = `${today}${String(state.lastPrefix || "").slice(6)}`;
|
||||
}
|
||||
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
const toNumber = (value, fallback) => {
|
||||
if (value == null) return fallback;
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? num : fallback;
|
||||
};
|
||||
|
||||
const toBool = (value, fallback) => {
|
||||
if (value == null) return fallback;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value === "string") {
|
||||
const v = value.trim().toLowerCase();
|
||||
if (["true", "1", "yes", "on"].includes(v)) return true;
|
||||
if (["false", "0", "no", "off"].includes(v)) return false;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const getSettingBool = (id, fallback) => {
|
||||
const raw =
|
||||
app?.extensionManager?.setting?.get?.(id) ??
|
||||
app?.ui?.settings?.getSettingValue?.(id) ??
|
||||
app?.ui?.settings?.settingsValues?.[id];
|
||||
return toBool(raw, fallback);
|
||||
};
|
||||
|
||||
function updateUI(state) {
|
||||
const safe = (fn, label) => {
|
||||
try {
|
||||
const result = fn?.();
|
||||
if (result && typeof result.then === "function") {
|
||||
result.catch((err) => {
|
||||
console.error(`[mjr] UI update failed: ${label}`, err);
|
||||
if (label === "status") {
|
||||
toast("error", "UI Error", `Failed to update ${label}`, { life: 3000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[mjr] UI update failed: ${label}`, err);
|
||||
// Show critical errors to user
|
||||
if (label === "status") {
|
||||
toast("error", "UI Error", `Failed to update ${label}`, { life: 3000 });
|
||||
}
|
||||
}
|
||||
};
|
||||
safe(state?._ui?.updateStatus, "status");
|
||||
safe(state?._ui?.updateResolve, "resolve");
|
||||
safe(state?._ui?.updateWorkflowBlock, "workflow");
|
||||
safe(state?._ui?.updateTargetLabel, "target label");
|
||||
safe(state?._ui?.refreshExistingNames, "existing names");
|
||||
safe(state?._ui?.refreshMissingStatus, "missing models");
|
||||
safe(state?._ui?.updatePreview, "preview");
|
||||
}
|
||||
|
||||
async function setActiveProjectById(state, projectId, token) {
|
||||
const resp = await fetchJSON("/mjr_project/list");
|
||||
const list = resp.projects || [];
|
||||
const entry = list.find((p) => p.project_id === projectId);
|
||||
if (!entry) {
|
||||
throw new Error("project not found");
|
||||
}
|
||||
if (token != null && state?.graphLoadToken !== token) {
|
||||
return null;
|
||||
}
|
||||
state.projectId = entry.project_id;
|
||||
state.projectFolder = entry.folder || "";
|
||||
state.projectExists = entry.exists !== false;
|
||||
state.lastError = "";
|
||||
_rollOutputDateIfNeeded(state);
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
state?._ui?.resetForProjectChange?.();
|
||||
|
||||
// Auto-patch output nodes when project is opened
|
||||
try {
|
||||
const relDir = state.lastRelDir || "";
|
||||
const prefix = state.lastPrefix || "";
|
||||
if (relDir || prefix) {
|
||||
const patched = await patchSaveNodes(app, relDir, prefix);
|
||||
if (patched > 0) {
|
||||
console.log(`[mjr] Auto-patched ${patched} save node(s) with project outputs`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[mjr] Failed to auto-patch nodes on project open:", err);
|
||||
}
|
||||
|
||||
toast("success", "Project active", entry.folder || entry.project_id);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared function to create and activate a project.
|
||||
* Eliminates code duplication across multiple create buttons.
|
||||
*/
|
||||
async function createAndActivateProject(state, projectName, updateCallbacks) {
|
||||
const {
|
||||
updateStatus,
|
||||
updateResolve,
|
||||
updateWorkflowBlock,
|
||||
resetForProjectChange,
|
||||
updatePreview,
|
||||
refreshProjectsList,
|
||||
} = updateCallbacks;
|
||||
|
||||
const resp = await fetchJSON("/mjr_project/set", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_name: projectName, create_base: true }),
|
||||
});
|
||||
|
||||
state.projectName = projectName;
|
||||
state.projectId = resp.project_id;
|
||||
state.projectFolder = resp.project_folder;
|
||||
state.projectExists = true;
|
||||
state.lastError = "";
|
||||
state.workflowHasSignature = true;
|
||||
|
||||
updateStatus();
|
||||
updateResolve();
|
||||
updateWorkflowBlock();
|
||||
saveState(state);
|
||||
resetForProjectChange();
|
||||
await updatePreview();
|
||||
refreshProjectsList(true);
|
||||
stampGraphProjectSignature(app, state);
|
||||
|
||||
// Auto-patch output nodes when project is created
|
||||
try {
|
||||
const relDir = state.lastRelDir || "";
|
||||
const prefix = state.lastPrefix || "";
|
||||
if (relDir || prefix) {
|
||||
const patched = await patchSaveNodes(app, relDir, prefix);
|
||||
if (patched > 0) {
|
||||
console.log(`[mjr] Auto-patched ${patched} save node(s) after project creation`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[mjr] Failed to auto-patch nodes on project creation:", err);
|
||||
}
|
||||
|
||||
toast("success", "Project created", resp.project_folder);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
const buildUpdateCallbacks = (state) => {
|
||||
const ui = state?._ui || {};
|
||||
return {
|
||||
updateStatus: ui.updateStatus || (() => {}),
|
||||
updateResolve: ui.updateResolve || (() => {}),
|
||||
updateWorkflowBlock: ui.updateWorkflowBlock || (() => {}),
|
||||
resetForProjectChange: ui.resetForProjectChange || (() => {}),
|
||||
updatePreview: ui.updatePreview || (async () => {}),
|
||||
refreshProjectsList: ui.refreshProjectsList || (() => {}),
|
||||
};
|
||||
};
|
||||
|
||||
async function promptEmptyGraphProject(state, runToken) {
|
||||
if (!state || state.projectId) return;
|
||||
if (state._emptyGraphPromptToken === runToken) return;
|
||||
state._emptyGraphPromptToken = runToken;
|
||||
|
||||
const confirmCreate = await psConfirm({
|
||||
title: "Empty workflow",
|
||||
message: "This workflow is empty. Create a new project now?\nCancel to set an existing project.",
|
||||
});
|
||||
if (state.graphLoadToken !== runToken) return;
|
||||
|
||||
if (confirmCreate) {
|
||||
const entered = await psPrompt({
|
||||
title: "Project name",
|
||||
message: "Enter a project name",
|
||||
defaultValue: state.pendingProjectName || "",
|
||||
});
|
||||
if (state.graphLoadToken !== runToken) return;
|
||||
if (entered === null) return;
|
||||
const projectName = String(entered || "").trim();
|
||||
if (!projectName) return;
|
||||
await createAndActivateProject(state, projectName, buildUpdateCallbacks(state));
|
||||
return;
|
||||
}
|
||||
|
||||
state.workflowPanelOpen = true;
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
state._ui?.refreshProjectsList?.(true);
|
||||
}
|
||||
|
||||
const isGraphCurrentlyEmpty = () => {
|
||||
const nodes = app?.graph?._nodes;
|
||||
return !Array.isArray(nodes) || nodes.length === 0;
|
||||
};
|
||||
|
||||
function handleGraphEmptyTransition(state, runToken, currentEmpty) {
|
||||
if (!state) return false;
|
||||
const wasEmpty = Boolean(state.graphIsEmpty);
|
||||
if (wasEmpty === currentEmpty) {
|
||||
if (!currentEmpty) {
|
||||
state._emptyGraphPromptToken = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
state.graphIsEmpty = currentEmpty;
|
||||
if (!currentEmpty) {
|
||||
state._emptyGraphPromptToken = null;
|
||||
} else {
|
||||
void promptEmptyGraphProject(state, runToken ?? state.graphLoadToken);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function startGraphEmptyWatcher(state) {
|
||||
if (!state) return;
|
||||
if (graphEmptyWatcherId) {
|
||||
clearInterval(graphEmptyWatcherId);
|
||||
graphEmptyWatcherId = null;
|
||||
}
|
||||
graphEmptyWatcherId = setInterval(() => {
|
||||
if (!state) return;
|
||||
const empty = isGraphCurrentlyEmpty();
|
||||
if (handleGraphEmptyTransition(state, state.graphLoadToken, empty)) {
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
}
|
||||
}, GRAPH_EMPTY_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function onGraphLoadedDetectProject(state, token) {
|
||||
if (!state) return;
|
||||
await loadConfig();
|
||||
const runToken = typeof token === "number" ? token : state.graphLoadToken || 0;
|
||||
const isStale = () => state.graphLoadToken !== runToken;
|
||||
const graphNodes = app?.graph?._nodes;
|
||||
const graphEmpty = !Array.isArray(graphNodes) || graphNodes.length === 0;
|
||||
const guardToast = (type, title, message, opts) => {
|
||||
if (!isStale() && !graphEmpty) {
|
||||
toast(type, title, message, opts);
|
||||
}
|
||||
};
|
||||
if (isStale()) return;
|
||||
|
||||
// Collect all state mutations in local variables
|
||||
let workflowHasSignature = false;
|
||||
const workflowChecked = true;
|
||||
let pendingProjectName = state.pendingProjectName || "";
|
||||
let pendingSelectProjectId = state.pendingSelectProjectId || "";
|
||||
let detected = {
|
||||
mode: "none",
|
||||
project_folder: "",
|
||||
project_id: "",
|
||||
confidence: 0,
|
||||
};
|
||||
let detectedModelTag = state.detectedModelTag || "";
|
||||
let lastError = "";
|
||||
|
||||
if (!graphEmpty) {
|
||||
const sig = readGraphSignature();
|
||||
if (sig && (sig.project_id || sig.project_folder)) {
|
||||
detected.mode = "signature";
|
||||
detected.project_id = String(sig.project_id || "");
|
||||
detected.project_folder = String(sig.project_folder || "");
|
||||
detected.confidence = 100;
|
||||
workflowHasSignature = true;
|
||||
guardToast(
|
||||
"info",
|
||||
"Project detected",
|
||||
`Workflow signature: ${detected.project_folder || detected.project_id}`
|
||||
);
|
||||
} else {
|
||||
const folder = inferProjectFolderFromGraph(isSaveLikeNode);
|
||||
if (folder) {
|
||||
detected.mode = "path";
|
||||
detected.project_folder = folder;
|
||||
detected.confidence = 60;
|
||||
guardToast("info", "Project inferred", `From save paths: ${folder}`);
|
||||
} else {
|
||||
guardToast("warn", "No project detected", "This workflow has no PROJECTS paths or signature.");
|
||||
}
|
||||
guardToast("warn", "Workflow unassigned", "Assign or create a project for this workflow.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowHasSignature) {
|
||||
if (detected.project_folder) {
|
||||
pendingProjectName = detected.project_folder;
|
||||
}
|
||||
pendingSelectProjectId = "";
|
||||
}
|
||||
|
||||
if (!detected.project_id && detected.project_folder) {
|
||||
detected.project_id = await tryResolveProjectId(detected.project_folder);
|
||||
if (isStale()) return;
|
||||
}
|
||||
|
||||
const activeId = state.projectId || "";
|
||||
const detectedId = detected.project_id || "";
|
||||
const hasActive = !!activeId;
|
||||
const conflict = hasActive && detectedId && activeId !== detectedId;
|
||||
|
||||
if (
|
||||
detected.mode === "signature" &&
|
||||
detectedId &&
|
||||
state.autoSwitchTrusted &&
|
||||
(!hasActive || conflict)
|
||||
) {
|
||||
if (isStale()) return;
|
||||
try {
|
||||
const entry = await setActiveProjectById(state, detectedId, runToken);
|
||||
if (isStale() || !entry) return;
|
||||
detected = { mode: "none", project_folder: "", project_id: "", confidence: 0 };
|
||||
} catch (e) {
|
||||
if (isStale()) return;
|
||||
lastError = String(e.message || e);
|
||||
guardToast("error", "Auto-switch failed", lastError);
|
||||
}
|
||||
} else if (conflict) {
|
||||
guardToast(
|
||||
"warn",
|
||||
"Project mismatch",
|
||||
`Workflow: ${detected.project_folder || detected.project_id}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!graphEmpty) {
|
||||
const modelTag = detectModelFromGraph(token3Tag) || "";
|
||||
if (modelTag !== state.detectedModelTag) {
|
||||
detectedModelTag = modelTag;
|
||||
if (modelTag) {
|
||||
guardToast("info", "Model detected", modelTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final staleness check before committing state mutations
|
||||
if (isStale()) return;
|
||||
|
||||
// Atomic state update - all mutations happen together
|
||||
state.workflowHasSignature = workflowHasSignature;
|
||||
state.workflowChecked = workflowChecked;
|
||||
state.pendingProjectName = pendingProjectName;
|
||||
state.pendingSelectProjectId = pendingSelectProjectId;
|
||||
state.detected = detected;
|
||||
state.workflowPanelOpen = graphEmpty ? false : !workflowHasSignature;
|
||||
handleGraphEmptyTransition(state, runToken, graphEmpty);
|
||||
state.detectedModelTag = detectedModelTag;
|
||||
if (lastError) {
|
||||
state.lastError = lastError;
|
||||
}
|
||||
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
}
|
||||
|
||||
async function saveWorkflowToProject(state, nameOverride) {
|
||||
if (!state?.projectId) {
|
||||
toast("error", "No active project", "Set a project before saving workflows.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get workflow BEFORE stamping signature to avoid partial state
|
||||
const workflow = getSerializedWorkflow();
|
||||
if (!workflow) {
|
||||
toast("error", "Workflow not available", "Could not serialize the current graph.");
|
||||
return;
|
||||
}
|
||||
|
||||
let workflowName = (nameOverride ?? state.workflowName ?? "").trim();
|
||||
if (!workflowName) {
|
||||
const suggestion = state?._ui?.buildWorkflowName?.() ?? "";
|
||||
const entered = await psPrompt({
|
||||
title: "Workflow name",
|
||||
message: "Enter a workflow name base",
|
||||
defaultValue: suggestion,
|
||||
});
|
||||
if (entered === null) {
|
||||
toast("info", "Workflow save cancelled", "No name provided.");
|
||||
return;
|
||||
}
|
||||
workflowName = String(entered || "").trim();
|
||||
if (!workflowName) {
|
||||
toast("error", "Validation", "Workflow name is required.");
|
||||
return;
|
||||
}
|
||||
state?._ui?.setWorkflowName?.(workflowName);
|
||||
}
|
||||
|
||||
const assetFolder = (state.workflowAsset || "").trim();
|
||||
|
||||
try {
|
||||
// Stamp signature before save attempt
|
||||
stampGraphProjectSignature(app, state);
|
||||
|
||||
const resp = await saveWorkflow({
|
||||
project_id: state.projectId,
|
||||
workflow_name: workflowName,
|
||||
workflow,
|
||||
overwrite: false,
|
||||
mirror_to_comfy_workflows: true,
|
||||
use_project_subfolder_in_workflows: true,
|
||||
asset_folder: assetFolder,
|
||||
});
|
||||
|
||||
// Only update state after successful save
|
||||
state.workflowHasSignature = true;
|
||||
state.workflowChecked = true;
|
||||
state.lastError = "";
|
||||
|
||||
const fileLabel = resp.file || "workflow.json";
|
||||
const projectInfo = resp.project_rel_path || "PROJECTS/<PROJECT>/03_WORKFLOWS";
|
||||
toast("success", "Workflow saved", `${fileLabel} -> ${projectInfo}`);
|
||||
|
||||
const fileBase = String(resp.file || "").replace(/\.json$/i, "");
|
||||
const suffixMatch = fileBase.match(/_(\d{4})$/);
|
||||
if (suffixMatch) {
|
||||
const base = fileBase.slice(0, -5);
|
||||
state.workflowLastBase = base;
|
||||
state.workflowNextSuffix = Number(suffixMatch[1]) + 1;
|
||||
state?._ui?.setWorkflowName?.(base);
|
||||
}
|
||||
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
|
||||
if (!resp.mirrored) {
|
||||
const reason = resp.mirror_error || "Mirror disabled or unavailable.";
|
||||
toast("warn", "Workflow mirror", reason);
|
||||
}
|
||||
} catch (e) {
|
||||
state.lastError = String(e.message || e);
|
||||
// Note: Signature was stamped, but save failed
|
||||
// User can try again with stamped signature
|
||||
updateUI(state);
|
||||
toast("error", "Workflow save failed", state.lastError);
|
||||
}
|
||||
}
|
||||
|
||||
function registerWorkflowShortcut() {
|
||||
if (workflowShortcutRegistered) return;
|
||||
workflowShortcutRegistered = true;
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
(event) => {
|
||||
const key = String(event.key || "").toLowerCase();
|
||||
if (!event.ctrlKey && !event.metaKey) return;
|
||||
if (key !== "s") return;
|
||||
const state = runtimeState;
|
||||
if (!state || state.autoHookSave === false) return;
|
||||
if (!state.projectId) return;
|
||||
if (event.cancelable) event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
saveWorkflowToProject(state);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const EXTENSION_SETTINGS = [
|
||||
{
|
||||
id: DOWNLOAD_MAX_GB_SETTING,
|
||||
name: "MajoorPS: Download max size (GB)",
|
||||
type: "number",
|
||||
defaultValue: DEFAULT_DOWNLOAD_MAX_GB,
|
||||
min: 1,
|
||||
max: 1024,
|
||||
step: 1,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
{
|
||||
id: AUTO_FIX_AFTER_DOWNLOAD_SETTING,
|
||||
name: "MajoorPS: Auto-run Fix after download",
|
||||
type: "boolean",
|
||||
defaultValue: DEFAULT_AUTO_FIX_AFTER_DOWNLOAD,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
{
|
||||
id: AUTO_OPEN_PANEL_ON_MISSING_SETTING,
|
||||
name: "MajoorPS: Auto-open panel on missing models",
|
||||
type: "boolean",
|
||||
defaultValue: DEFAULT_AUTO_OPEN_PANEL,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
{
|
||||
id: AUTO_SCAN_MISSING_ON_OPEN_SETTING,
|
||||
name: "MajoorPS: Auto-scan missing on panel open",
|
||||
type: "boolean",
|
||||
defaultValue: DEFAULT_AUTO_SCAN_MISSING_ON_OPEN,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
{
|
||||
id: AUTO_REMEMBER_NOTE_SOURCES_SETTING,
|
||||
name: "MajoorPS: Auto-remember note sources",
|
||||
type: "boolean",
|
||||
defaultValue: DEFAULT_AUTO_REMEMBER_NOTE_SOURCES,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
{
|
||||
id: NO_PROJECT_STATUS_BLACK_SETTING,
|
||||
name: "MajoorPS: Black status when no project",
|
||||
type: "boolean",
|
||||
defaultValue: DEFAULT_NO_PROJECT_STATUS_BLACK,
|
||||
category: SETTINGS_CATEGORY,
|
||||
},
|
||||
];
|
||||
|
||||
function registerLegacyExtensionSettings() {
|
||||
if (app?.extensionManager?.setting) return;
|
||||
const settings = app?.ui?.settings;
|
||||
if (!settings?.addSetting) return;
|
||||
try {
|
||||
const values = settings.settingsValues || {};
|
||||
for (const setting of EXTENSION_SETTINGS) {
|
||||
const fallback =
|
||||
setting.type === "number"
|
||||
? toNumber(values[setting.id], setting.defaultValue)
|
||||
: toBool(values[setting.id], setting.defaultValue);
|
||||
settings.addSetting({ ...setting, defaultValue: fallback });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[mjr] settings registration failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Majoor.ProjectSettings",
|
||||
settings: EXTENSION_SETTINGS,
|
||||
|
||||
async setup() {
|
||||
await loadConfig();
|
||||
registerLegacyExtensionSettings();
|
||||
const canSidebar = !!app?.extensionManager?.registerSidebarTab;
|
||||
const state = createRuntimeState(loadState() || {});
|
||||
|
||||
if (!state.kind) state.kind = "asset";
|
||||
if (state.media === "asset") {
|
||||
state.kind = "asset";
|
||||
state.media = "images";
|
||||
}
|
||||
if (state.media === "shot") {
|
||||
state.kind = "shot";
|
||||
state.media = "videos";
|
||||
}
|
||||
if (state.customModel && !state.customModelText) {
|
||||
state.customModelText = state.customModel;
|
||||
}
|
||||
if (state.modelSelection === "__custom__") {
|
||||
state.modelSelection = "";
|
||||
}
|
||||
if (state.pathTemplate === "{BASE}/{MEDIA}/{DATE}/{MODEL}/{NAME}") {
|
||||
state.pathTemplate = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}";
|
||||
}
|
||||
if (state.useCustomModel == null) {
|
||||
state.useCustomModel = false;
|
||||
}
|
||||
if (state.autoHookSave == null) {
|
||||
state.autoHookSave = true;
|
||||
}
|
||||
if (!state.workflowLastBase) {
|
||||
state.workflowLastBase = "";
|
||||
}
|
||||
const nextSuffix = Number(state.workflowNextSuffix);
|
||||
if (!nextSuffix || nextSuffix < 1) {
|
||||
state.workflowNextSuffix = 1;
|
||||
} else {
|
||||
state.workflowNextSuffix = Math.floor(nextSuffix);
|
||||
}
|
||||
if (state.modelUpper == null) {
|
||||
state.modelUpper = state.useCustomModel === true;
|
||||
}
|
||||
if (!state.detectedModelTag) {
|
||||
state.detectedModelTag = "";
|
||||
}
|
||||
if (!Number.isFinite(state.graphLoadToken)) {
|
||||
state.graphLoadToken = 0;
|
||||
}
|
||||
setRuntimeState(state);
|
||||
_rollOutputDateIfNeeded(state);
|
||||
registerWorkflowShortcut();
|
||||
state._emptyGraphPromptToken = null;
|
||||
startGraphEmptyWatcher(state);
|
||||
|
||||
const renderPanel = (el) =>
|
||||
buildPanel(el, state, {
|
||||
createAndActivateProject,
|
||||
setActiveProjectById,
|
||||
saveWorkflowToProject,
|
||||
});
|
||||
|
||||
if (canSidebar) {
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "mjr_project_settings",
|
||||
icon: "pi pi-briefcase",
|
||||
title: "Project",
|
||||
tooltip: "Majoor Project Settings",
|
||||
type: "custom",
|
||||
render: (el) => renderPanel(el),
|
||||
});
|
||||
} else {
|
||||
const box = document.createElement("div");
|
||||
box.style.position = "fixed";
|
||||
box.style.top = "64px";
|
||||
box.style.right = "16px";
|
||||
box.style.zIndex = "9999";
|
||||
box.style.background = "rgba(20,20,20,0.9)";
|
||||
box.style.border = "1px solid rgba(255,255,255,0.1)";
|
||||
box.style.padding = "10px";
|
||||
box.style.borderRadius = "10px";
|
||||
box.style.width = "280px";
|
||||
document.body.appendChild(box);
|
||||
renderPanel(box);
|
||||
}
|
||||
},
|
||||
|
||||
async beforeConfigureGraph() {
|
||||
if (runtimeState) {
|
||||
runtimeState.isGraphLoading = true;
|
||||
runtimeState.graphLoadToken = (runtimeState.graphLoadToken || 0) + 1;
|
||||
}
|
||||
},
|
||||
|
||||
async afterConfigureGraph() {
|
||||
if (runtimeState) {
|
||||
runtimeState.isGraphLoading = false;
|
||||
onGraphLoadedDetectProject(runtimeState, runtimeState.graphLoadToken);
|
||||
_rollOutputDateIfNeeded(runtimeState);
|
||||
|
||||
// Automatically scan for missing models when workflow loads (deferred to avoid blocking graph configure)
|
||||
if (getSettingBool(AUTO_SCAN_MISSING_ON_OPEN_SETTING, DEFAULT_AUTO_SCAN_MISSING_ON_OPEN)) {
|
||||
const scanToken = runtimeState.graphLoadToken;
|
||||
setTimeout(() => {
|
||||
if (!runtimeState || runtimeState.graphLoadToken !== scanToken) return;
|
||||
try {
|
||||
const { missing } = scanMissingModelsWithStats();
|
||||
|
||||
if (runtimeState?._ui?.refreshMissingStatus) {
|
||||
runtimeState._ui.refreshMissingStatus();
|
||||
}
|
||||
|
||||
if (missing && missing.length > 0) {
|
||||
toast(
|
||||
"warn",
|
||||
"Missing Models",
|
||||
`${missing.length} missing model${missing.length !== 1 ? "s" : ""} detected in workflow`,
|
||||
{ life: 4000 }
|
||||
);
|
||||
|
||||
if (getSettingBool(AUTO_OPEN_PANEL_ON_MISSING_SETTING, DEFAULT_AUTO_OPEN_PANEL)) {
|
||||
const sidebar = app?.extensionManager?.sidebar;
|
||||
if (sidebar && typeof sidebar.setTab === "function") {
|
||||
setTimeout(() => {
|
||||
sidebar.setTab("mjr_project_settings");
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[mjr] Error scanning missing models on workflow load:", error);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async beforeQueuePrompt() {
|
||||
if (runtimeState) {
|
||||
// Roll date if needed before execution
|
||||
_rollOutputDateIfNeeded(runtimeState);
|
||||
|
||||
// Auto-patch output nodes with current date before execution
|
||||
if (runtimeState.projectId && (runtimeState.lastRelDir || runtimeState.lastPrefix)) {
|
||||
try {
|
||||
const relDir = runtimeState.lastRelDir || "";
|
||||
const prefix = runtimeState.lastPrefix || "";
|
||||
const patched = await patchSaveNodes(app, relDir, prefix);
|
||||
if (patched > 0) {
|
||||
console.log(`[mjr] Auto-patched ${patched} save node(s) before execution`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[mjr] Failed to auto-patch nodes before execution:", err);
|
||||
}
|
||||
}
|
||||
|
||||
stampGraphProjectSignature(app, runtimeState);
|
||||
}
|
||||
},
|
||||
|
||||
async nodeCreated(node) {
|
||||
const state = runtimeState;
|
||||
if (!state || state.isGraphLoading) return;
|
||||
if (state.graphIsEmpty) {
|
||||
if (handleGraphEmptyTransition(state, state.graphLoadToken, false)) {
|
||||
saveState(state);
|
||||
updateUI(state);
|
||||
}
|
||||
}
|
||||
await loadConfig();
|
||||
if (!state.projectId || !state.autoPatchNewNodes) return;
|
||||
if (!state.projectFolder || state.projectExists === false) return;
|
||||
|
||||
if (!isSaveLikeNode(node)) return;
|
||||
|
||||
let warnedPathed = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const attemptPatch = (retriesLeft) => {
|
||||
// Check if node was removed from graph
|
||||
const nodeStillExists = app.graph?._nodes?.includes(node);
|
||||
if (!nodeStillExists) {
|
||||
// Clean up timeout if node was removed
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.projectId || !state.autoPatchNewNodes) return;
|
||||
if (!state.projectFolder || state.projectExists === false) return;
|
||||
if (state.isGraphLoading) return;
|
||||
|
||||
const widgets = node?.widgets || [];
|
||||
const filenameWidget = widgets.find((w) => w?.name === "filename_prefix");
|
||||
const pathWidget = widgets.find((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
if (!filenameWidget && !pathWidget) {
|
||||
if (retriesLeft > 0) {
|
||||
timeoutId = setTimeout(() => attemptPatch(retriesLeft - 1), NODE_PATCH_RETRY_DELAY_MS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (filenameWidget && alreadyProjectPathed(filenameWidget.value)) {
|
||||
if (!warnedPathed) {
|
||||
warnedPathed = true;
|
||||
toast("warn", "Patch skipped", "Node already points to a project path.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pathWidget && alreadyProjectPathed(pathWidget.value)) {
|
||||
if (!warnedPathed) {
|
||||
warnedPathed = true;
|
||||
toast("warn", "Patch skipped", "Node already points to a project path.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nm = detectNodeMedia(node) || "images";
|
||||
_rollOutputDateIfNeeded(state);
|
||||
let relDir = state.lastRelDir;
|
||||
let prefix = state.lastPrefix || "";
|
||||
if (!relDir) {
|
||||
const base = `PROJECTS/${state.projectFolder}`;
|
||||
relDir = joinRel(base, mediaDir(nm));
|
||||
}
|
||||
if (patchSingleNode(node, relDir, prefix)) {
|
||||
try {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
} catch (_) {}
|
||||
toast("success", "Auto-patch", "New save node patched.", { life: 2000 });
|
||||
} else if (retriesLeft > 0) {
|
||||
timeoutId = setTimeout(() => attemptPatch(retriesLeft - 1), NODE_PATCH_RETRY_DELAY_MS);
|
||||
}
|
||||
};
|
||||
|
||||
attemptPatch(NODE_PATCH_MAX_RETRIES);
|
||||
},
|
||||
});
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
let _csrfTokenPromise = null;
|
||||
|
||||
function _getApiKey() {
|
||||
try {
|
||||
const key = localStorage.getItem("mjr_api_key");
|
||||
return (key || "").trim();
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function _getCsrfToken(forceRefresh = false) {
|
||||
if (forceRefresh) {
|
||||
_csrfTokenPromise = null;
|
||||
}
|
||||
if (_csrfTokenPromise) return _csrfTokenPromise;
|
||||
_csrfTokenPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetch("/mjr_security/csrf", {
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
console.error("[MJR] Failed to get CSRF token:", data?.error || `HTTP ${resp.status}`);
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
const token = data?.csrf_token || "";
|
||||
if (!token) {
|
||||
console.error("[MJR] CSRF token is empty");
|
||||
}
|
||||
return token;
|
||||
} catch (err) {
|
||||
console.error("[MJR] Error fetching CSRF token:", err);
|
||||
_csrfTokenPromise = null; // Reset on error
|
||||
throw err;
|
||||
}
|
||||
})();
|
||||
return _csrfTokenPromise;
|
||||
}
|
||||
|
||||
export async function fetchJSON(url, opts) {
|
||||
const o = opts ? { ...opts } : {};
|
||||
o.credentials = o.credentials || "same-origin";
|
||||
o.headers = { ...(o.headers || {}) };
|
||||
|
||||
const apiKey = _getApiKey();
|
||||
if (apiKey && !o.headers["X-MJR-API-Key"] && !o.headers["Authorization"]) {
|
||||
o.headers["X-MJR-API-Key"] = apiKey;
|
||||
}
|
||||
|
||||
const method = String(o.method || "GET").toUpperCase();
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
const csrf = await _getCsrfToken();
|
||||
if (csrf) {
|
||||
o.headers["X-CSRF-Token"] = csrf;
|
||||
} else {
|
||||
console.warn("[MJR] No CSRF token available for request");
|
||||
}
|
||||
}
|
||||
|
||||
const resp = await fetch(url, o);
|
||||
|
||||
// If we get a 403 (Forbidden), it might be a CSRF issue - retry once with fresh token
|
||||
if (resp.status === 403 && ["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
console.warn("[MJR] Got 403, retrying with fresh CSRF token...");
|
||||
const freshCsrf = await _getCsrfToken(true);
|
||||
if (freshCsrf && freshCsrf !== o.headers["X-CSRF-Token"]) {
|
||||
o.headers["X-CSRF-Token"] = freshCsrf;
|
||||
const retryResp = await fetch(url, o);
|
||||
const retryData = await retryResp.json().catch(() => ({}));
|
||||
if (!retryResp.ok || retryData?.ok === false) {
|
||||
throw new Error(retryData?.error || `HTTP ${retryResp.status}`);
|
||||
}
|
||||
return retryData;
|
||||
}
|
||||
}
|
||||
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data?.ok === false) {
|
||||
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function tryResolveProjectId(folder) {
|
||||
try {
|
||||
const data = await fetchJSON(`/mjr_project/resolve?folder=${encodeURIComponent(folder)}`);
|
||||
if (data?.ok) return data.project_id || "";
|
||||
} catch (_) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function listProjects() {
|
||||
return fetchJSON("/mjr_project/list");
|
||||
}
|
||||
|
||||
export async function setProject(project_name, create_base = true) {
|
||||
return fetchJSON("/mjr_project/set", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ project_name, create_base }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getModels() {
|
||||
return fetchJSON("/mjr_project/models");
|
||||
}
|
||||
|
||||
export async function createCustomOut(payload) {
|
||||
return fetchJSON("/mjr_project/create_custom_out", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function previewTemplate(template, tokens) {
|
||||
return fetchJSON("/mjr_project/preview_template", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ template, tokens: tokens || {} }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listExistingNames(project_id, media = "images") {
|
||||
const qs = new URLSearchParams({
|
||||
project_id: project_id || "",
|
||||
media: media || "images",
|
||||
});
|
||||
const data = await fetchJSON(`/mjr_project/assets/list_names?${qs.toString()}`);
|
||||
return Array.isArray(data?.names) ? data.names : [];
|
||||
}
|
||||
|
||||
export async function saveWorkflow(payload) {
|
||||
return fetchJSON("/mjr_project/workflow/save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function scanModelCandidates(missing) {
|
||||
return fetchJSON("/mjr_models/scan_candidates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveModelRecipes(missing) {
|
||||
return fetchJSON("/mjr_models/resolve_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ missing: missing || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveModelRecipes(items) {
|
||||
return fetchJSON("/mjr_models/save_recipes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadModels(items) {
|
||||
return fetchJSON("/mjr_models/download", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: items || [] }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDownloadStatus(job_id) {
|
||||
const qs = new URLSearchParams({ job_id: job_id || "" });
|
||||
return fetchJSON(`/mjr_models/download_status?${qs.toString()}`);
|
||||
}
|
||||
|
||||
export async function getFingerprintCacheStatus() {
|
||||
return fetchJSON("/mjr_models/fingerprint_cache_status");
|
||||
}
|
||||
|
||||
export async function buildFingerprintCache(payload) {
|
||||
return fetchJSON("/mjr_models/build_fingerprint_cache", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveFingerprint(payload) {
|
||||
return fetchJSON("/mjr_models/resolve_by_fingerprint", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function searchRegistry(query, limit = 6) {
|
||||
return fetchJSON("/mjr_models/registry/search", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: String(query || ""), limit }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function contributeRegistry(payload) {
|
||||
return fetchJSON("/mjr_models/registry/contribute", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload || {}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
export async function psPrompt({ title, message, defaultValue } = {}) {
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.prompt) {
|
||||
try {
|
||||
const result = await dialog.prompt({
|
||||
title: title || "Input",
|
||||
message: message || "",
|
||||
defaultValue: defaultValue ?? "",
|
||||
default: defaultValue ?? "",
|
||||
});
|
||||
if (result === null || result === undefined) return null;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
const value = result.value;
|
||||
return value == null ? null : String(value);
|
||||
}
|
||||
return String(result);
|
||||
} catch (_) {}
|
||||
}
|
||||
const fallback = window.prompt(message || title || "", defaultValue ?? "");
|
||||
if (fallback === null) return null;
|
||||
return String(fallback);
|
||||
}
|
||||
|
||||
export async function psConfirm({ title, message } = {}) {
|
||||
const dialog = app?.extensionManager?.dialog;
|
||||
if (dialog?.confirm) {
|
||||
try {
|
||||
const result = await dialog.confirm({
|
||||
title: title || "Confirm",
|
||||
message: message || "",
|
||||
});
|
||||
if (typeof result === "boolean") return result;
|
||||
if (typeof result === "object" && "value" in result) {
|
||||
return Boolean(result.value);
|
||||
}
|
||||
return Boolean(result);
|
||||
} catch (_) {}
|
||||
}
|
||||
return window.confirm(message || title || "Confirm?");
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
function getInnerGraphFromNode(node) {
|
||||
if (!node) return null;
|
||||
const candidates = [
|
||||
node.subgraph,
|
||||
node._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue;
|
||||
if (Array.isArray(candidate?._nodes)) return candidate;
|
||||
if (Array.isArray(candidate?.graph?._nodes)) return candidate.graph;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function scanGraphForModel(graph, token3TagFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes)) return "";
|
||||
if (visited.has(graph)) return "";
|
||||
visited.add(graph);
|
||||
|
||||
const keys = [
|
||||
"ckpt",
|
||||
"checkpoint",
|
||||
"checkpoint_name",
|
||||
"ckpt_name",
|
||||
"model",
|
||||
"model_name",
|
||||
"diffusion",
|
||||
"diffusion_model",
|
||||
"unet",
|
||||
"unet_name",
|
||||
"gguf",
|
||||
];
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const name = String(w?.name || "").toLowerCase();
|
||||
if (!name) continue;
|
||||
if (!keys.some((k) => name.includes(k))) continue;
|
||||
const val = w?.value;
|
||||
if (typeof val !== "string") continue;
|
||||
const tag = token3TagFn ? token3TagFn(val, false) : "";
|
||||
if (tag) {
|
||||
if (DEBUG) console.debug("[mjr] model tag found", tag);
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG) console.debug("[mjr] scanning subgraph for model");
|
||||
const subTag = scanGraphForModel(inner, token3TagFn, visited);
|
||||
if (subTag) return subTag;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function scanGraphForProjectFolder(graph, isSaveLikeNodeFn, visited) {
|
||||
if (!graph || !Array.isArray(graph._nodes)) return "";
|
||||
if (visited.has(graph)) return "";
|
||||
visited.add(graph);
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (isSaveLikeNodeFn && !isSaveLikeNodeFn(node)) continue;
|
||||
const widgets = node?.widgets || [];
|
||||
for (const w of widgets) {
|
||||
const v = w?.value;
|
||||
if (typeof v === "string" || typeof v === "number") {
|
||||
const s = String(v).replace(/\\/g, "/");
|
||||
const m = s.match(/PROJECTS\/([^/]+)/i);
|
||||
if (m && m[1]) return m[1];
|
||||
}
|
||||
}
|
||||
const inner = getInnerGraphFromNode(node);
|
||||
if (inner) {
|
||||
if (DEBUG) console.debug("[mjr] scanning subgraph for project folder");
|
||||
const subFolder = scanGraphForProjectFolder(inner, isSaveLikeNodeFn, visited);
|
||||
if (subFolder) return subFolder;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function readGraphSignature() {
|
||||
const g = app?.graph;
|
||||
if (!g) return null;
|
||||
const direct = g?.extra?.mjr_project;
|
||||
if (direct && typeof direct === "object") return direct;
|
||||
try {
|
||||
const ser = g?.serialize?.();
|
||||
const sig = ser?.extra?.mjr_project;
|
||||
if (sig && typeof sig === "object") return sig;
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function inferProjectFolderFromGraph(isSaveLikeNodeFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForProjectFolder(g, isSaveLikeNodeFn, new WeakSet());
|
||||
}
|
||||
|
||||
export function detectModelFromGraph(token3TagFn) {
|
||||
const g = app?.graph;
|
||||
return scanGraphForModel(g, token3TagFn, new WeakSet());
|
||||
}
|
||||
|
||||
export function getSerializedWorkflow() {
|
||||
const g = app?.graph;
|
||||
if (!g || typeof g.serialize !== "function") return null;
|
||||
try {
|
||||
const ser = g.serialize();
|
||||
if (g.extra && typeof g.extra === "object") {
|
||||
ser.extra = ser.extra || {};
|
||||
Object.assign(ser.extra, g.extra);
|
||||
}
|
||||
return ser;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Simple logging wrapper for Majoor Project Settings UI
|
||||
export const MJR_DEBUG = (typeof window !== 'undefined' && Boolean(window?.MJR_DEBUG)) || false;
|
||||
|
||||
export function debug(...args) {
|
||||
if (MJR_DEBUG) {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function info(...args) {
|
||||
console.info(...args);
|
||||
}
|
||||
|
||||
export function warn(...args) {
|
||||
console.warn(...args);
|
||||
}
|
||||
|
||||
export function error(...args) {
|
||||
console.error(...args);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import { ensureStyles, toast } from "./toast.js";
|
||||
import { contributeRegistry } from "./api.js";
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: "huggingface", label: "HuggingFace" },
|
||||
{ value: "civitai", label: "CivitAI" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
{ value: "modelscope", label: "ModelScope" },
|
||||
{ value: "other", label: "Other" },
|
||||
];
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
];
|
||||
|
||||
class ModelContributionDialog {
|
||||
constructor() {
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
}
|
||||
|
||||
show(modelName = "") {
|
||||
this.close();
|
||||
ensureStyles();
|
||||
this._render(modelName);
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.dialog) {
|
||||
this.dialog.remove();
|
||||
this.dialog = null;
|
||||
this.statusEl = null;
|
||||
this.submitBtn = null;
|
||||
this.validateBtn = null;
|
||||
this.inputs = {};
|
||||
}
|
||||
}
|
||||
|
||||
_render(modelName) {
|
||||
this.dialog = document.createElement("div");
|
||||
this.dialog.className = "mjr-contribution-dialog";
|
||||
Object.assign(this.dialog.style, {
|
||||
position: "fixed",
|
||||
left: "0",
|
||||
top: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: "9999",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
Object.assign(overlay.style, {
|
||||
position: "absolute",
|
||||
inset: "0",
|
||||
background: "rgba(0,0,0,0.65)",
|
||||
backdropFilter: "blur(4px)",
|
||||
});
|
||||
|
||||
const panel = document.createElement("div");
|
||||
Object.assign(panel.style, {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
background: "rgba(15, 18, 26, 0.95)",
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
borderRadius: "16px",
|
||||
padding: "24px",
|
||||
width: "360px",
|
||||
color: "#f2f4f8",
|
||||
fontFamily: "inherit",
|
||||
pointerEvents: "auto",
|
||||
boxShadow: "0 20px 50px rgba(0,0,0,0.4)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
});
|
||||
|
||||
panel.innerHTML = `
|
||||
<h2 style="margin:0;font-size:18px;letter-spacing:0.5px;text-transform:uppercase;">Share a model URL</h2>
|
||||
<p style="margin:0;font-size:12px;color:#a0b3d6;">Help others by contributing a verified download link.</p>
|
||||
`;
|
||||
|
||||
const fields = [
|
||||
{ id: "name", label: "Model name", placeholder: "Stable Diffusion 1.5" },
|
||||
{ id: "url", label: "Download URL", placeholder: "https://huggingface.co/.../model.safetensors" },
|
||||
];
|
||||
|
||||
for (const field of fields) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = field.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.placeholder = field.placeholder;
|
||||
input.style.padding = "8px 10px";
|
||||
input.style.borderRadius = "8px";
|
||||
input.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
input.style.background = "rgba(255,255,255,0.04)";
|
||||
input.style.color = "#f4f6fb";
|
||||
input.style.fontSize = "13px";
|
||||
|
||||
if (field.id === "name" && modelName) {
|
||||
input.value = modelName;
|
||||
}
|
||||
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[field.id] = input;
|
||||
}
|
||||
|
||||
const selects = [
|
||||
{ id: "platform", label: "Platform", options: PLATFORM_OPTIONS },
|
||||
{ id: "type", label: "Model type", options: TYPE_OPTIONS },
|
||||
];
|
||||
|
||||
for (const selectCfg of selects) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.style.display = "flex";
|
||||
wrap.style.flexDirection = "column";
|
||||
wrap.style.gap = "4px";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = selectCfg.label;
|
||||
label.style.fontSize = "11px";
|
||||
label.style.opacity = "0.8";
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.style.padding = "8px 10px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.15)";
|
||||
select.style.background = "rgba(255,255,255,0.04)";
|
||||
select.style.color = "#f4f6fb";
|
||||
select.style.fontSize = "13px";
|
||||
|
||||
for (const option of selectCfg.options) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(select);
|
||||
panel.appendChild(wrap);
|
||||
this.inputs[selectCfg.id] = select;
|
||||
}
|
||||
|
||||
const extraWrap = document.createElement("div");
|
||||
extraWrap.style.display = "flex";
|
||||
extraWrap.style.flexDirection = "column";
|
||||
extraWrap.style.gap = "4px";
|
||||
|
||||
const fileLabel = document.createElement("label");
|
||||
fileLabel.textContent = "Filename (optional)";
|
||||
fileLabel.style.fontSize = "11px";
|
||||
fileLabel.style.opacity = "0.8";
|
||||
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "text";
|
||||
fileInput.placeholder = "model.safetensors";
|
||||
Object.assign(fileInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
|
||||
const shaLabel = document.createElement("label");
|
||||
shaLabel.textContent = "SHA256 (optional)";
|
||||
shaLabel.style.fontSize = "11px";
|
||||
shaLabel.style.opacity = "0.8";
|
||||
|
||||
const shaInput = document.createElement("input");
|
||||
shaInput.type = "text";
|
||||
shaInput.placeholder = "abc123...";
|
||||
Object.assign(shaInput.style, {
|
||||
padding: "8px 10px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
color: "#f4f6fb",
|
||||
fontSize: "13px",
|
||||
});
|
||||
|
||||
extraWrap.appendChild(fileLabel);
|
||||
extraWrap.appendChild(fileInput);
|
||||
extraWrap.appendChild(shaLabel);
|
||||
extraWrap.appendChild(shaInput);
|
||||
panel.appendChild(extraWrap);
|
||||
this.inputs.filename = fileInput;
|
||||
this.inputs.sha256 = shaInput;
|
||||
|
||||
this.statusEl = document.createElement("div");
|
||||
this.statusEl.style.minHeight = "20px";
|
||||
this.statusEl.style.fontSize = "12px";
|
||||
this.statusEl.style.opacity = "0.85";
|
||||
panel.appendChild(this.statusEl);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "space-between";
|
||||
|
||||
this.validateBtn = document.createElement("button");
|
||||
this.validateBtn.textContent = "Validate URL";
|
||||
this.validateBtn.className = "mjr-ps-btn";
|
||||
this.validateBtn.style.flex = "1";
|
||||
this.validateBtn.type = "button";
|
||||
|
||||
this.submitBtn = document.createElement("button");
|
||||
this.submitBtn.textContent = "Contribute";
|
||||
this.submitBtn.className = "mjr-ps-btn";
|
||||
this.submitBtn.style.flex = "1";
|
||||
this.submitBtn.type = "button";
|
||||
|
||||
actions.appendChild(this.validateBtn);
|
||||
actions.appendChild(this.submitBtn);
|
||||
panel.appendChild(actions);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "Cancel";
|
||||
closeBtn.type = "button";
|
||||
closeBtn.style.alignSelf = "flex-end";
|
||||
closeBtn.style.fontSize = "12px";
|
||||
closeBtn.style.background = "transparent";
|
||||
closeBtn.style.border = "none";
|
||||
closeBtn.style.color = "#9ab1d8";
|
||||
|
||||
panel.appendChild(closeBtn);
|
||||
|
||||
this.dialog.appendChild(overlay);
|
||||
this.dialog.appendChild(panel);
|
||||
document.body.appendChild(this.dialog);
|
||||
|
||||
this.validateBtn.addEventListener("click", () => this._validateUrl());
|
||||
this.submitBtn.addEventListener("click", () => this._submit());
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
overlay.addEventListener("click", () => this.close());
|
||||
}
|
||||
|
||||
_setStatus(message, tone = "info") {
|
||||
if (!this.statusEl) return;
|
||||
this.statusEl.textContent = message;
|
||||
const colors = {
|
||||
info: "#a0b3d6",
|
||||
success: "#84ff9c",
|
||||
error: "#f87c7c",
|
||||
};
|
||||
this.statusEl.style.color = colors[tone] || colors.info;
|
||||
}
|
||||
|
||||
_validateUrl() {
|
||||
const value = (this.inputs.url?.value || "").trim();
|
||||
if (!value) {
|
||||
this._setStatus("Enter the download URL first.", "error");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(value)) {
|
||||
this._setStatus("URL must start with http:// or https://", "error");
|
||||
return;
|
||||
}
|
||||
this._setStatus("URL format looks good. You can submit!", "success");
|
||||
}
|
||||
|
||||
async _submit() {
|
||||
if (!this.submitBtn || !this.dialog) return;
|
||||
const name = (this.inputs.name?.value || "").trim();
|
||||
const url = (this.inputs.url?.value || "").trim();
|
||||
if (!name || !url) {
|
||||
this._setStatus("Model name and URL are required.", "error");
|
||||
return;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
this._setStatus("URL must start with http(s)", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitBtn.disabled = true;
|
||||
this.validateBtn.disabled = true;
|
||||
this._setStatus("Sending contribution...", "info");
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
url,
|
||||
platform: this.inputs.platform?.value || "unknown",
|
||||
type: this.inputs.type?.value || "checkpoints",
|
||||
filename: (this.inputs.filename?.value || "").trim(),
|
||||
sha256: (this.inputs.sha256?.value || "").trim(),
|
||||
};
|
||||
const resp = await contributeRegistry(payload);
|
||||
const added = resp?.added || false;
|
||||
this._setStatus(added ? "Contribution recorded, thanks!" : "Source already exists.", "success");
|
||||
toast("success", "Contribution", added ? "Thank you for sharing!" : "Source already in registry.");
|
||||
if (added) {
|
||||
setTimeout(() => this.close(), 1600);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Contribution failed", error);
|
||||
this._setStatus("Failed to send contribution.", "error");
|
||||
toast("error", "Contribution failed", String(error?.message || error));
|
||||
} finally {
|
||||
this.submitBtn.disabled = false;
|
||||
this.validateBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _dialogInstance = null;
|
||||
|
||||
export function showModelContributionDialog(modelName = "") {
|
||||
if (!_dialogInstance) {
|
||||
_dialogInstance = new ModelContributionDialog();
|
||||
}
|
||||
_dialogInstance.show(modelName);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const KIND_OPTIONS = [
|
||||
{ value: "checkpoints", label: "Checkpoint" },
|
||||
{ value: "diffusion_models", label: "Diffusion Model" },
|
||||
{ value: "loras", label: "LoRA" },
|
||||
{ value: "vae", label: "VAE" },
|
||||
{ value: "controlnet", label: "ControlNet" },
|
||||
{ value: "text_encoders", label: "Text Encoder" },
|
||||
{ value: "clip", label: "CLIP" },
|
||||
{ value: "clip_vision", label: "CLIP Vision" },
|
||||
{ value: "unet", label: "UNet" },
|
||||
{ value: "upscale_models", label: "Upscale Model" },
|
||||
{ value: "embeddings", label: "Embedding" },
|
||||
];
|
||||
|
||||
const TYPE_HINT_KIND = {
|
||||
checkpoint: "checkpoints",
|
||||
diffusion: "diffusion_models",
|
||||
diffusion_models: "diffusion_models",
|
||||
lora: "loras",
|
||||
vae: "vae",
|
||||
controlnet: "controlnet",
|
||||
upscale_models: "upscale_models",
|
||||
text_encoder: "text_encoders",
|
||||
text_encoders: "text_encoders",
|
||||
clip: "clip",
|
||||
clip_vision: "clip_vision",
|
||||
unet: "unet",
|
||||
embeddings: "embeddings",
|
||||
unknown: "",
|
||||
};
|
||||
|
||||
import { collectRecipesFromWorkflowNotes } from "./workflow_note_recipes.js";
|
||||
|
||||
const ALLOWED_EXTENSIONS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin"];
|
||||
|
||||
export function getKindOptions() {
|
||||
return KIND_OPTIONS.slice();
|
||||
}
|
||||
|
||||
export function typeHintToKind(typeHint) {
|
||||
const key = String(typeHint || "").toLowerCase();
|
||||
return TYPE_HINT_KIND[key] || "";
|
||||
}
|
||||
|
||||
export function getAllowedExtensions() {
|
||||
return ALLOWED_EXTENSIONS.slice();
|
||||
}
|
||||
|
||||
export function isValidUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFilenameFromUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(String(url || ""));
|
||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAllowedExtension(filename) {
|
||||
const name = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||
}
|
||||
|
||||
export function normalizeKey(value) {
|
||||
const text = String(value || "").replace(/\\/g, "/");
|
||||
const base = text.split("/").pop() || "";
|
||||
return base.split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
export function collectNoteRecipes(workflowJson) {
|
||||
return collectRecipesFromWorkflowNotes(workflowJson);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const MAX_MISSING = 200;
|
||||
const VALUE_SET_THRESHOLD = 256;
|
||||
|
||||
// Widgets to exclude from model detection (images, etc.)
|
||||
const IMAGE_WIDGET_NAMES = new Set([
|
||||
"image",
|
||||
"images",
|
||||
"input_image",
|
||||
"image_path",
|
||||
"mask",
|
||||
"background",
|
||||
"foreground",
|
||||
]);
|
||||
|
||||
const FILE_WIDGET_NAME_HINTS = [
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"mask",
|
||||
"path",
|
||||
"file",
|
||||
"filename",
|
||||
"directory",
|
||||
"folder",
|
||||
"output",
|
||||
];
|
||||
|
||||
const FILE_EXTENSIONS = [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".webp",
|
||||
".gif",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".mp4",
|
||||
".webm",
|
||||
".mov",
|
||||
".mkv",
|
||||
".avi",
|
||||
".mp3",
|
||||
".wav",
|
||||
".flac",
|
||||
".ogg",
|
||||
];
|
||||
|
||||
function isImageWidget(widgetName) {
|
||||
const lower = String(widgetName || "").toLowerCase();
|
||||
if (IMAGE_WIDGET_NAMES.has(lower)) return true;
|
||||
if (lower.includes("png") || lower.includes("jpg") || lower.includes("jpeg")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function looksLikeFileWidget(widgetName, value) {
|
||||
const name = String(widgetName || "").toLowerCase();
|
||||
if (FILE_WIDGET_NAME_HINTS.some((h) => name.includes(h))) return true;
|
||||
const v = String(value || "").toLowerCase().trim();
|
||||
return FILE_EXTENSIONS.some((ext) => v.endsWith(ext));
|
||||
}
|
||||
|
||||
function typeHintFromNode(node, widgetName) {
|
||||
const nodeType = String(node?.type || node?.title || "").toLowerCase();
|
||||
const widgetLower = String(widgetName || "").toLowerCase();
|
||||
const nodeHasClipVision =
|
||||
nodeType.includes("clip_vision") ||
|
||||
nodeType.includes("clip vision") ||
|
||||
(nodeType.includes("clip") && nodeType.includes("vision"));
|
||||
|
||||
// Match based on node type (highest priority)
|
||||
if (nodeHasClipVision) return "clip_vision";
|
||||
if (nodeType.includes("checkpoint")) {
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
return "checkpoint";
|
||||
}
|
||||
if (nodeType.includes("loader")) {
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
if (widgetLower.includes("unet") || widgetLower.includes("diffusion")) return "unet";
|
||||
if (widgetLower.includes("text_encoder") || widgetLower.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("clip")) return "clip";
|
||||
}
|
||||
if (nodeType.includes("lora")) return "lora";
|
||||
if (nodeType.includes("vae")) return "vae";
|
||||
if (nodeType.includes("controlnet") || nodeType.includes("control")) return "controlnet";
|
||||
if (nodeType.includes("upscale")) return "upscale_models";
|
||||
if (nodeType.includes("clip")) {
|
||||
if (widgetLower.includes("vision")) return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (nodeType.includes("text_encoder") || nodeType.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (nodeType.includes("unet")) return "unet";
|
||||
if (nodeType.includes("embedding") || nodeType.includes("textual")) return "embeddings";
|
||||
|
||||
// Match based on widget name (secondary priority)
|
||||
if (widgetLower.includes("ckpt") || widgetLower.includes("checkpoint")) return "checkpoint";
|
||||
if (widgetLower.includes("lora")) return "lora";
|
||||
if (widgetLower.includes("vae")) return "vae";
|
||||
if (widgetLower.includes("controlnet") || widgetLower.includes("control")) return "controlnet";
|
||||
if (widgetLower.includes("upscale")) return "upscale_models";
|
||||
if (widgetLower.includes("clip")) {
|
||||
if (widgetLower.includes("vision")) return "clip_vision";
|
||||
return "clip";
|
||||
}
|
||||
if (widgetLower.includes("text_encoder") || widgetLower.includes("text encoder"))
|
||||
return "text_encoders";
|
||||
if (widgetLower.includes("diffusion")) return "diffusion_models";
|
||||
if (widgetLower.includes("unet")) return "unet";
|
||||
if (widgetLower.includes("embedding")) return "embeddings";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function isModelWidget(node, widgetName, value) {
|
||||
if (isImageWidget(widgetName)) return false;
|
||||
if (looksLikeFileWidget(widgetName, value)) return false;
|
||||
return typeHintFromNode(node, widgetName) !== "unknown";
|
||||
}
|
||||
|
||||
// Recursively collect all nodes including those in subgraphs and groups
|
||||
function collectAllNodes(graph) {
|
||||
const allNodes = [];
|
||||
const visited = new WeakSet();
|
||||
|
||||
function collectFromGraph(g) {
|
||||
if (!g || !Array.isArray(g._nodes)) return;
|
||||
|
||||
for (const node of g._nodes) {
|
||||
if (!node || typeof node !== "object") continue;
|
||||
if (visited.has(node)) continue;
|
||||
visited.add(node);
|
||||
allNodes.push(node);
|
||||
|
||||
// Check for subgraph
|
||||
if (node.subgraph && typeof node.subgraph === "object") {
|
||||
collectFromGraph(node.subgraph);
|
||||
}
|
||||
|
||||
// Check for group nodes (some implementations)
|
||||
if (node.graph && typeof node.graph === "object" && node.graph !== g) {
|
||||
collectFromGraph(node.graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectFromGraph(graph);
|
||||
return allNodes;
|
||||
}
|
||||
|
||||
function _valuesContain(values, value, widget) {
|
||||
if (!Array.isArray(values) || values.length === 0) return false;
|
||||
|
||||
const valueStr = String(value);
|
||||
if (values.length < VALUE_SET_THRESHOLD) {
|
||||
for (const item of values) {
|
||||
if (String(item) === valueStr) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!widget || typeof widget !== "object") {
|
||||
const set = new Set(values.map((v) => String(v)));
|
||||
return set.has(valueStr);
|
||||
}
|
||||
|
||||
if (widget.__mjrValuesRef !== values || !(widget.__mjrValuesSet instanceof Set)) {
|
||||
widget.__mjrValuesRef = values;
|
||||
widget.__mjrValuesSet = new Set(values.map((v) => String(v)));
|
||||
}
|
||||
return widget.__mjrValuesSet.has(valueStr);
|
||||
}
|
||||
|
||||
export function scanMissingModelsFromGraph() {
|
||||
return scanMissingModelsWithStats().missing;
|
||||
}
|
||||
|
||||
export function scanMissingModelsWithStats() {
|
||||
const graph = app?.graph;
|
||||
if (!graph) return { missing: [], total: 0 };
|
||||
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
|
||||
const missing = [];
|
||||
const nodesWithMissing = new Set();
|
||||
let total = 0;
|
||||
|
||||
for (const node of allNodes) {
|
||||
const widgets = node?.widgets || [];
|
||||
for (const widget of widgets) {
|
||||
const widgetName = widget?.name || "";
|
||||
|
||||
const values = widget?.options?.values;
|
||||
if (!Array.isArray(values)) continue;
|
||||
const value = widget?.value;
|
||||
if (!value) continue;
|
||||
|
||||
// Only scan model selector widgets (skip image/video/file/path widgets, etc.)
|
||||
if (!isModelWidget(node, widgetName, value)) continue;
|
||||
|
||||
total += 1;
|
||||
if (_valuesContain(values, value, widget)) continue;
|
||||
|
||||
missing.push({
|
||||
node_id: node?.id ?? null,
|
||||
node_title: node?.title || node?.type || "",
|
||||
widget_name: widgetName,
|
||||
missing_value: String(value),
|
||||
type_hint: typeHintFromNode(node, widgetName),
|
||||
});
|
||||
|
||||
// Track nodes with missing models
|
||||
if (node?.id != null) {
|
||||
nodesWithMissing.add(node.id);
|
||||
}
|
||||
|
||||
if (missing.length >= MAX_MISSING) break;
|
||||
}
|
||||
if (missing.length >= MAX_MISSING) break;
|
||||
}
|
||||
|
||||
// Highlight nodes with missing models
|
||||
highlightNodesWithMissingModels(allNodes, nodesWithMissing);
|
||||
|
||||
return { missing, total };
|
||||
}
|
||||
|
||||
function highlightNodesWithMissingModels(allNodes, nodesWithMissing) {
|
||||
for (const node of allNodes) {
|
||||
if (!node) continue;
|
||||
|
||||
if (nodesWithMissing.has(node.id)) {
|
||||
if (!node.__mjrMissingHighlight) {
|
||||
node.__mjrMissingHighlight = true;
|
||||
node.__mjrMissingColor = node.color ?? null;
|
||||
node.__mjrMissingBgcolor = node.bgcolor ?? null;
|
||||
}
|
||||
// Highlight node with missing models (border only)
|
||||
node.color = "#FF0000";
|
||||
node.bgcolor = null;
|
||||
} else if (node.__mjrMissingHighlight) {
|
||||
// Restore original colors if no longer missing
|
||||
node.color = node.__mjrMissingColor ?? null;
|
||||
node.bgcolor = node.__mjrMissingBgcolor ?? null;
|
||||
delete node.__mjrMissingHighlight;
|
||||
delete node.__mjrMissingColor;
|
||||
delete node.__mjrMissingBgcolor;
|
||||
}
|
||||
}
|
||||
|
||||
// Force canvas redraw
|
||||
if (typeof app?.graph?.setDirtyCanvas === "function") {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyFixesToGraph(fixes) {
|
||||
const graph = app?.graph;
|
||||
if (!graph) return 0;
|
||||
|
||||
// Collect all nodes including subgraphs and groups
|
||||
const allNodes = collectAllNodes(graph);
|
||||
|
||||
const nodesById = new Map();
|
||||
for (const node of allNodes) {
|
||||
if (node && node.id != null) {
|
||||
nodesById.set(node.id, node);
|
||||
}
|
||||
}
|
||||
|
||||
let fixed = 0;
|
||||
const fixedNodeIds = new Set();
|
||||
|
||||
for (const fix of fixes || []) {
|
||||
if (!fix) continue;
|
||||
const node = nodesById.get(fix.node_id);
|
||||
if (!node) continue;
|
||||
const widgets = node?.widgets || [];
|
||||
const widget = widgets.find((w) => w?.name === fix.widget_name);
|
||||
if (!widget) continue;
|
||||
widget.value = fix.new_value;
|
||||
// Ensure widget callbacks fire so nodes update internal state.
|
||||
if (typeof widget.callback === "function") {
|
||||
try {
|
||||
widget.callback(widget.value, app.graph, node, widget);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (typeof node.onWidgetChanged === "function") {
|
||||
try {
|
||||
node.onWidgetChanged(widget.name, widget.value, widget);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (typeof node.onPropertyChanged === "function") {
|
||||
try {
|
||||
node.onPropertyChanged(widget.name, widget.value, widget);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (Array.isArray(node.widgets)) {
|
||||
node.widgets_values = node.widgets.map((w) => w?.value);
|
||||
}
|
||||
if (typeof node.setDirtyCanvas === "function") {
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
fixed += 1;
|
||||
fixedNodeIds.add(fix.node_id);
|
||||
}
|
||||
|
||||
// Clear highlighting on fixed nodes
|
||||
for (const nodeId of fixedNodeIds) {
|
||||
const node = nodesById.get(nodeId);
|
||||
if (node && node.color === "#FF0000") {
|
||||
node.color = null;
|
||||
node.bgcolor = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixed > 0 && typeof graph.setDirtyCanvas === "function") {
|
||||
graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
if (fixed > 0 && typeof app?.refreshComboInNodes === "function") {
|
||||
try {
|
||||
app.refreshComboInNodes();
|
||||
} catch (_) {}
|
||||
}
|
||||
return fixed;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { fetchJSON } from "./api.js";
|
||||
import { safeRel, joinRel } from "./utils.js";
|
||||
|
||||
export let PATH_WIDGETS = [];
|
||||
let configPromise = null;
|
||||
|
||||
export async function loadConfig() {
|
||||
if (configPromise) return configPromise;
|
||||
configPromise = (async () => {
|
||||
try {
|
||||
const resp = await fetchJSON("/mjr_project/config");
|
||||
const list = Array.isArray(resp?.path_widgets) ? resp.path_widgets : [];
|
||||
PATH_WIDGETS = list.map((item) => String(item)).filter(Boolean);
|
||||
return PATH_WIDGETS;
|
||||
} catch (err) {
|
||||
console.error("[mjr] Failed to load config:", err);
|
||||
return PATH_WIDGETS;
|
||||
} finally {
|
||||
configPromise = null;
|
||||
}
|
||||
})();
|
||||
return configPromise;
|
||||
}
|
||||
|
||||
export function detectNodeMedia(node) {
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (t.includes("video") || t.includes("mp4") || t.includes("gif")) return "videos";
|
||||
if (t.includes("image") || t.includes("png") || t.includes("jpg") || t.includes("webp"))
|
||||
return "images";
|
||||
|
||||
const widgets = node?.widgets || [];
|
||||
const hasVideoHints = widgets.some((w) =>
|
||||
["frame_rate", "fps", "duration", "frames_per_second"].includes(String(w?.name || ""))
|
||||
);
|
||||
return hasVideoHints ? "videos" : "images";
|
||||
}
|
||||
|
||||
export function isSaveLikeNode(node) {
|
||||
const t = String(node?.comfyClass || node?.type || "").toLowerCase();
|
||||
if (!t.includes("save") && !t.includes("export") && !t.includes("combine")) return false;
|
||||
const widgets = node?.widgets || [];
|
||||
const hasFilename = widgets.some((w) => w?.name === "filename_prefix");
|
||||
const hasPath = widgets.some((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
return hasFilename || hasPath;
|
||||
}
|
||||
|
||||
export function alreadyProjectPathed(value) {
|
||||
if (!value) return false;
|
||||
const s = String(value).replace(/\\/g, "/");
|
||||
return s.includes("PROJECTS/") || s.includes("02_OUT/") || s.includes("00_META/");
|
||||
}
|
||||
|
||||
export function patchSingleNode(node, relDir, filenamePrefix) {
|
||||
if (!isSaveLikeNode(node)) return false;
|
||||
|
||||
const rel = safeRel(relDir);
|
||||
const prefix = safeRel(filenamePrefix);
|
||||
const widgets = node?.widgets || [];
|
||||
|
||||
const filenameWidget = widgets.find((w) => w?.name === "filename_prefix");
|
||||
const pathWidget = widgets.find((w) => w?.name && PATH_WIDGETS.includes(w.name));
|
||||
|
||||
if (pathWidget && filenameWidget) {
|
||||
pathWidget.value = rel;
|
||||
filenameWidget.value = prefix;
|
||||
return true;
|
||||
}
|
||||
if (filenameWidget) {
|
||||
filenameWidget.value = prefix ? joinRel(rel, prefix) : rel;
|
||||
return true;
|
||||
}
|
||||
if (pathWidget) {
|
||||
pathWidget.value = rel;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function patchSaveNodes(app, relDir, filenamePrefix, targetMedia) {
|
||||
await loadConfig();
|
||||
const nodes = app?.graph?._nodes || [];
|
||||
let patched = 0;
|
||||
for (const node of nodes) {
|
||||
const nm = detectNodeMedia(node);
|
||||
if (targetMedia && nm && nm !== targetMedia) continue;
|
||||
if (patchSingleNode(node, relDir, filenamePrefix)) {
|
||||
patched += 1;
|
||||
}
|
||||
}
|
||||
try {
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
} catch (_) {}
|
||||
return patched;
|
||||
}
|
||||
|
||||
export function stampGraphProjectSignature(app, state) {
|
||||
if (!state?.projectId || !state?.projectFolder) return;
|
||||
const g = app?.graph;
|
||||
if (!g) return;
|
||||
g.extra = g.extra || {};
|
||||
g.extra.mjr_project = {
|
||||
project_id: state.projectId,
|
||||
project_folder: state.projectFolder,
|
||||
template: state.pathTemplate || "",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
export function ensureStyles() {
|
||||
if (document.getElementById("mjr-project-settings-style")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "mjr-project-settings-style";
|
||||
style.textContent = `
|
||||
.mjr-ps-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
will-change: transform;
|
||||
transition: transform 80ms ease, background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
.mjr-ps-btn:hover {
|
||||
background: rgba(46, 136, 255, 0.25);
|
||||
border-color: rgba(46, 136, 255, 0.6);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
.mjr-ps-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
.mjr-ps-btn:focus-visible {
|
||||
outline: 2px solid rgba(46, 136, 255, 0.6);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.mjr-ps-select {
|
||||
background: rgba(12, 14, 18, 0.7);
|
||||
color: #f2f4f8;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
color-scheme: dark;
|
||||
appearance: none;
|
||||
}
|
||||
.mjr-ps-select option {
|
||||
background: #121418 !important;
|
||||
color: #f2f4f8 !important;
|
||||
}
|
||||
.mjr-ps-select optgroup {
|
||||
background: #101218 !important;
|
||||
color: #b6c0d3 !important;
|
||||
}
|
||||
.mjr-ps-select option:hover,
|
||||
.mjr-ps-select option:checked {
|
||||
background: #1a2230 !important;
|
||||
color: #f2f4f8 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
let toastContainer = null;
|
||||
|
||||
function getToastContainer() {
|
||||
if (toastContainer && document.body.contains(toastContainer)) return toastContainer;
|
||||
let el = document.getElementById("mjr-ps-toast-container");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "mjr-ps-toast-container";
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "16px";
|
||||
el.style.right = "16px";
|
||||
el.style.zIndex = "10000";
|
||||
el.style.display = "flex";
|
||||
el.style.flexDirection = "column";
|
||||
el.style.gap = "8px";
|
||||
el.style.pointerEvents = "none";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
toastContainer = el;
|
||||
return el;
|
||||
}
|
||||
|
||||
export function toast(type, title, message, opts = {}) {
|
||||
const map = { success: "success", info: "info", warn: "warn", error: "error" };
|
||||
const severity = map[type] || "info";
|
||||
try {
|
||||
const mgr = app?.extensionManager?.toast;
|
||||
if (mgr?.add) {
|
||||
mgr.add({
|
||||
severity,
|
||||
summary: title || "",
|
||||
detail: message || "",
|
||||
life: Number(opts.life || 4000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
const container = getToastContainer();
|
||||
const item = document.createElement("div");
|
||||
const colors = {
|
||||
success: "rgba(46, 143, 78, 0.9)",
|
||||
info: "rgba(46, 136, 255, 0.9)",
|
||||
warn: "rgba(199, 124, 42, 0.9)",
|
||||
error: "rgba(179, 58, 58, 0.9)",
|
||||
};
|
||||
item.style.background = colors[severity] || colors.info;
|
||||
item.style.color = "#f2f4f8";
|
||||
item.style.padding = "8px 10px";
|
||||
item.style.borderRadius = "8px";
|
||||
item.style.boxShadow = "0 6px 16px rgba(0,0,0,0.3)";
|
||||
item.style.maxWidth = "320px";
|
||||
item.style.pointerEvents = "none";
|
||||
item.style.backdropFilter = "blur(6px)";
|
||||
|
||||
const strong = document.createElement("div");
|
||||
strong.textContent = title || "";
|
||||
strong.style.fontWeight = "600";
|
||||
strong.style.marginBottom = message ? "2px" : "0";
|
||||
const detail = document.createElement("div");
|
||||
detail.textContent = message || "";
|
||||
detail.style.opacity = "0.9";
|
||||
detail.style.fontSize = "12px";
|
||||
|
||||
item.appendChild(strong);
|
||||
if (message) item.appendChild(detail);
|
||||
container.appendChild(item);
|
||||
|
||||
const life = Number(opts.life || 4000);
|
||||
setTimeout(() => {
|
||||
if (item.parentNode) item.remove();
|
||||
}, life);
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
import { ensureStyles } from "./toast.js";
|
||||
import {
|
||||
extractFilenameFromUrl,
|
||||
getAllowedExtensions,
|
||||
hasAllowedExtension,
|
||||
isValidUrl,
|
||||
} from "./model_downloader.js";
|
||||
|
||||
function formatRecipeText(recipe) {
|
||||
const parts = [];
|
||||
if (recipe?.url) parts.push(recipe.url);
|
||||
if (recipe?.filename) parts.push(`file: ${recipe.filename}`);
|
||||
if (recipe?.kind) parts.push(`kind: ${recipe.kind}`);
|
||||
if (recipe?.sha256) parts.push(`sha256: ${recipe.sha256}`);
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
function showSearchResultsDialog(results, callback) {
|
||||
ensureStyles();
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.inset = "0";
|
||||
overlay.style.background = "rgba(5, 6, 10, 0.85)";
|
||||
overlay.style.zIndex = "10011";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.padding = "16px";
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.style.width = "min(600px, 90vw)";
|
||||
panel.style.maxHeight = "70vh";
|
||||
panel.style.overflow = "auto";
|
||||
panel.style.background = "rgba(18, 20, 26, 0.98)";
|
||||
panel.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.padding = "16px";
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.gap = "12px";
|
||||
panel.style.color = "#eee";
|
||||
panel.style.fontSize = "12px";
|
||||
panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.5)";
|
||||
overlay.appendChild(panel);
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = `Search Results (${results.length})`;
|
||||
title.style.fontWeight = "600";
|
||||
title.style.fontSize = "14px";
|
||||
title.style.letterSpacing = "0.4px";
|
||||
panel.appendChild(title);
|
||||
|
||||
const resultsList = document.createElement("div");
|
||||
resultsList.style.display = "flex";
|
||||
resultsList.style.flexDirection = "column";
|
||||
resultsList.style.gap = "10px";
|
||||
|
||||
for (const result of results) {
|
||||
const resultCard = document.createElement("div");
|
||||
resultCard.style.padding = "10px";
|
||||
resultCard.style.border = "1px solid rgba(255,255,255,0.1)";
|
||||
resultCard.style.borderRadius = "8px";
|
||||
resultCard.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
resultCard.style.cursor = "pointer";
|
||||
resultCard.style.transition = "all 0.2s";
|
||||
|
||||
resultCard.addEventListener("mouseenter", () => {
|
||||
resultCard.style.border = "1px solid rgba(46, 136, 255, 0.6)";
|
||||
resultCard.style.background = "rgba(46, 136, 255, 0.1)";
|
||||
});
|
||||
|
||||
resultCard.addEventListener("mouseleave", () => {
|
||||
resultCard.style.border = "1px solid rgba(255,255,255,0.1)";
|
||||
resultCard.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
});
|
||||
|
||||
const nameRow = document.createElement("div");
|
||||
nameRow.style.fontWeight = "600";
|
||||
nameRow.style.marginBottom = "6px";
|
||||
nameRow.style.display = "flex";
|
||||
nameRow.style.alignItems = "center";
|
||||
nameRow.style.gap = "8px";
|
||||
nameRow.style.flexWrap = "wrap";
|
||||
|
||||
// Match score badge
|
||||
const scoreBadge = document.createElement("span");
|
||||
const score = result.match_score || 0;
|
||||
const matchLevel = result.match_level || "";
|
||||
scoreBadge.textContent = `${Math.round(score)}%`;
|
||||
scoreBadge.title = matchLevel;
|
||||
scoreBadge.style.fontSize = "9px";
|
||||
scoreBadge.style.padding = "2px 6px";
|
||||
scoreBadge.style.borderRadius = "4px";
|
||||
scoreBadge.style.fontWeight = "700";
|
||||
|
||||
// Color code by score
|
||||
if (score >= 90) {
|
||||
scoreBadge.style.background = "rgba(46, 204, 113, 0.25)";
|
||||
scoreBadge.style.border = "1px solid rgba(46, 204, 113, 0.6)";
|
||||
scoreBadge.style.color = "#7fffc4";
|
||||
} else if (score >= 70) {
|
||||
scoreBadge.style.background = "rgba(52, 152, 219, 0.25)";
|
||||
scoreBadge.style.border = "1px solid rgba(52, 152, 219, 0.6)";
|
||||
scoreBadge.style.color = "#99d9ff";
|
||||
} else if (score >= 50) {
|
||||
scoreBadge.style.background = "rgba(241, 196, 15, 0.25)";
|
||||
scoreBadge.style.border = "1px solid rgba(241, 196, 15, 0.6)";
|
||||
scoreBadge.style.color = "#ffe680";
|
||||
} else {
|
||||
scoreBadge.style.background = "rgba(231, 76, 60, 0.25)";
|
||||
scoreBadge.style.border = "1px solid rgba(231, 76, 60, 0.6)";
|
||||
scoreBadge.style.color = "#ffb3a8";
|
||||
}
|
||||
|
||||
const platformBadge = document.createElement("span");
|
||||
platformBadge.textContent = result.platform?.toUpperCase() || "UNKNOWN";
|
||||
platformBadge.style.fontSize = "9px";
|
||||
platformBadge.style.padding = "2px 6px";
|
||||
platformBadge.style.borderRadius = "4px";
|
||||
platformBadge.style.fontWeight = "700";
|
||||
|
||||
if (result.platform === "civitai") {
|
||||
platformBadge.style.background = "rgba(255, 99, 71, 0.2)";
|
||||
platformBadge.style.border = "1px solid rgba(255, 99, 71, 0.5)";
|
||||
platformBadge.style.color = "#ffb3a3";
|
||||
} else if (result.platform === "huggingface") {
|
||||
platformBadge.style.background = "rgba(255, 215, 0, 0.2)";
|
||||
platformBadge.style.border = "1px solid rgba(255, 215, 0, 0.5)";
|
||||
platformBadge.style.color = "#ffe680";
|
||||
} else if (result.platform === "github") {
|
||||
platformBadge.style.background = "rgba(138, 43, 226, 0.2)";
|
||||
platformBadge.style.border = "1px solid rgba(138, 43, 226, 0.5)";
|
||||
platformBadge.style.color = "#d8b3ff";
|
||||
} else if (result.platform === "web_search" || result.platform === "web") {
|
||||
platformBadge.style.background = "rgba(52, 152, 219, 0.2)";
|
||||
platformBadge.style.border = "1px solid rgba(52, 152, 219, 0.5)";
|
||||
platformBadge.style.color = "#99d9ff";
|
||||
platformBadge.textContent = "WEB";
|
||||
}
|
||||
|
||||
const nameText = document.createElement("span");
|
||||
nameText.textContent = result.name || "Unnamed";
|
||||
nameText.style.flex = "1";
|
||||
|
||||
nameRow.appendChild(scoreBadge);
|
||||
nameRow.appendChild(platformBadge);
|
||||
nameRow.appendChild(nameText);
|
||||
|
||||
const infoRow = document.createElement("div");
|
||||
infoRow.style.fontSize = "10px";
|
||||
infoRow.style.opacity = "0.8";
|
||||
infoRow.style.marginTop = "4px";
|
||||
|
||||
const infoParts = [];
|
||||
if (result.type) infoParts.push(`Type: ${result.type}`);
|
||||
if (result.version) infoParts.push(`Ver: ${result.version}`);
|
||||
if (result.size_mb) infoParts.push(`Size: ${result.size_mb}MB`);
|
||||
if (result.filename) infoParts.push(`File: ${result.filename}`);
|
||||
|
||||
infoRow.textContent = infoParts.join(" | ");
|
||||
|
||||
resultCard.appendChild(nameRow);
|
||||
resultCard.appendChild(infoRow);
|
||||
|
||||
resultCard.addEventListener("click", () => {
|
||||
overlay.remove();
|
||||
callback(result);
|
||||
});
|
||||
|
||||
resultsList.appendChild(resultCard);
|
||||
}
|
||||
|
||||
panel.appendChild(resultsList);
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.className = "mjr-ps-btn";
|
||||
cancelBtn.style.padding = "6px 12px";
|
||||
cancelBtn.style.borderRadius = "8px";
|
||||
cancelBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
cancelBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
cancelBtn.style.color = "#eee";
|
||||
cancelBtn.style.cursor = "pointer";
|
||||
cancelBtn.style.alignSelf = "flex-end";
|
||||
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
overlay.remove();
|
||||
callback(null);
|
||||
});
|
||||
|
||||
panel.appendChild(cancelBtn);
|
||||
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (e.target === overlay) {
|
||||
overlay.remove();
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
export function showModelDownloaderDialog({ entries, kindOptions, existingMap } = {}) {
|
||||
ensureStyles();
|
||||
const items = Array.isArray(entries) ? entries : [];
|
||||
const kinds = Array.isArray(kindOptions) ? kindOptions : [];
|
||||
const existing = existingMap instanceof Map ? existingMap : null;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.inset = "0";
|
||||
overlay.style.background = "rgba(5, 6, 10, 0.75)";
|
||||
overlay.style.zIndex = "10010";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.padding = "16px";
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.style.width = "min(720px, 92vw)";
|
||||
panel.style.maxHeight = "80vh";
|
||||
panel.style.overflow = "auto";
|
||||
panel.style.background = "rgba(18, 20, 26, 0.95)";
|
||||
panel.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.padding = "14px 16px";
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.gap = "10px";
|
||||
panel.style.color = "#eee";
|
||||
panel.style.fontSize = "12px";
|
||||
panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.4)";
|
||||
overlay.appendChild(panel);
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Download Missing Models";
|
||||
title.style.fontWeight = "600";
|
||||
title.style.fontSize = "13px";
|
||||
title.style.letterSpacing = "0.4px";
|
||||
panel.appendChild(title);
|
||||
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.textContent = `Missing entries: ${items.length}`;
|
||||
subtitle.style.opacity = "0.8";
|
||||
subtitle.style.fontSize = "11px";
|
||||
panel.appendChild(subtitle);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.style.display = "flex";
|
||||
list.style.flexDirection = "column";
|
||||
list.style.gap = "8px";
|
||||
panel.appendChild(list);
|
||||
|
||||
const rows = [];
|
||||
for (const entry of items) {
|
||||
const recipe = entry?.recipe || null;
|
||||
const isPrefilled = entry?.prefilled === true || !!recipe;
|
||||
const isNoteRecipe = recipe?.source === "workflow_note";
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.style.display = "flex";
|
||||
row.style.flexDirection = "column";
|
||||
row.style.gap = "6px";
|
||||
row.style.padding = "8px";
|
||||
row.style.borderRadius = "8px";
|
||||
row.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||
row.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
const defaultBorder = row.style.border;
|
||||
const defaultBackground = row.style.background;
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.style.display = "flex";
|
||||
header.style.alignItems = "center";
|
||||
header.style.justifyContent = "space-between";
|
||||
header.style.gap = "8px";
|
||||
|
||||
const titleWrap = document.createElement("div");
|
||||
titleWrap.style.display = "flex";
|
||||
titleWrap.style.alignItems = "center";
|
||||
titleWrap.style.gap = "6px";
|
||||
const titleText = document.createElement("div");
|
||||
titleText.textContent = entry?.missing_value || entry?.key || "Missing model";
|
||||
titleText.style.fontWeight = "600";
|
||||
titleWrap.appendChild(titleText);
|
||||
|
||||
if (isNoteRecipe) {
|
||||
const badge = document.createElement("span");
|
||||
badge.textContent = "From Workflow Note";
|
||||
badge.style.fontSize = "10px";
|
||||
badge.style.padding = "1px 6px";
|
||||
badge.style.borderRadius = "999px";
|
||||
badge.style.background = "rgba(46, 136, 255, 0.18)";
|
||||
badge.style.border = "1px solid rgba(46, 136, 255, 0.45)";
|
||||
badge.style.color = "#cfe3ff";
|
||||
titleWrap.appendChild(badge);
|
||||
}
|
||||
|
||||
const downloadWrap = document.createElement("label");
|
||||
downloadWrap.style.display = "flex";
|
||||
downloadWrap.style.alignItems = "center";
|
||||
downloadWrap.style.gap = "6px";
|
||||
downloadWrap.style.cursor = "pointer";
|
||||
const downloadCb = document.createElement("input");
|
||||
downloadCb.type = "checkbox";
|
||||
downloadCb.checked = isPrefilled;
|
||||
const downloadText = document.createElement("span");
|
||||
downloadText.textContent = "Download";
|
||||
downloadText.style.opacity = "0.85";
|
||||
downloadWrap.appendChild(downloadCb);
|
||||
downloadWrap.appendChild(downloadText);
|
||||
|
||||
header.appendChild(titleWrap);
|
||||
header.appendChild(downloadWrap);
|
||||
|
||||
row.appendChild(header);
|
||||
const separator = document.createElement("div");
|
||||
separator.style.height = "1px";
|
||||
separator.style.background = "rgba(255,255,255,0.08)";
|
||||
separator.style.margin = "2px 0";
|
||||
row.appendChild(separator);
|
||||
|
||||
const urlLabel = document.createElement("div");
|
||||
urlLabel.textContent = "URL";
|
||||
urlLabel.style.opacity = "0.8";
|
||||
urlLabel.style.fontSize = "10px";
|
||||
const urlField = document.createElement("input");
|
||||
urlField.type = "text";
|
||||
urlField.placeholder = "https://...";
|
||||
urlField.style.width = "100%";
|
||||
urlField.style.padding = "6px 8px";
|
||||
urlField.style.borderRadius = "8px";
|
||||
urlField.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
urlField.style.background = "rgba(0,0,0,0.25)";
|
||||
urlField.style.color = "#eee";
|
||||
|
||||
const kindLabel = document.createElement("div");
|
||||
kindLabel.textContent = "Kind";
|
||||
kindLabel.style.opacity = "0.8";
|
||||
kindLabel.style.fontSize = "10px";
|
||||
const kindField = document.createElement("select");
|
||||
kindField.className = "mjr-ps-select";
|
||||
kindField.style.width = "100%";
|
||||
kindField.style.padding = "6px 8px";
|
||||
kindField.style.borderRadius = "8px";
|
||||
kindField.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
kindField.style.color = "#f2f4f8";
|
||||
for (const option of kinds) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = option.value;
|
||||
opt.textContent = option.label || option.value;
|
||||
kindField.appendChild(opt);
|
||||
}
|
||||
|
||||
const fileLabel = document.createElement("div");
|
||||
fileLabel.textContent = "Filename (optional)";
|
||||
fileLabel.style.opacity = "0.8";
|
||||
fileLabel.style.fontSize = "10px";
|
||||
const fileField = document.createElement("input");
|
||||
fileField.type = "text";
|
||||
fileField.placeholder = getAllowedExtensions().join(", ");
|
||||
fileField.style.width = "100%";
|
||||
fileField.style.padding = "6px 8px";
|
||||
fileField.style.borderRadius = "8px";
|
||||
fileField.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
fileField.style.background = "rgba(0,0,0,0.25)";
|
||||
fileField.style.color = "#eee";
|
||||
|
||||
const shaLabel = document.createElement("div");
|
||||
shaLabel.textContent = "SHA256 (optional)";
|
||||
shaLabel.style.opacity = "0.8";
|
||||
shaLabel.style.fontSize = "10px";
|
||||
const shaField = document.createElement("input");
|
||||
shaField.type = "text";
|
||||
shaField.placeholder = "64 hex chars";
|
||||
shaField.style.width = "100%";
|
||||
shaField.style.padding = "6px 8px";
|
||||
shaField.style.borderRadius = "8px";
|
||||
shaField.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
shaField.style.background = "rgba(0,0,0,0.25)";
|
||||
shaField.style.color = "#eee";
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.style.display = "grid";
|
||||
grid.style.gridTemplateColumns = "1fr 1fr";
|
||||
grid.style.gap = "6px";
|
||||
|
||||
const colLeft = document.createElement("div");
|
||||
colLeft.style.display = "flex";
|
||||
colLeft.style.flexDirection = "column";
|
||||
colLeft.style.gap = "4px";
|
||||
colLeft.appendChild(urlLabel);
|
||||
colLeft.appendChild(urlField);
|
||||
colLeft.appendChild(fileLabel);
|
||||
colLeft.appendChild(fileField);
|
||||
|
||||
const colRight = document.createElement("div");
|
||||
colRight.style.display = "flex";
|
||||
colRight.style.flexDirection = "column";
|
||||
colRight.style.gap = "4px";
|
||||
colRight.appendChild(kindLabel);
|
||||
colRight.appendChild(kindField);
|
||||
colRight.appendChild(shaLabel);
|
||||
colRight.appendChild(shaField);
|
||||
|
||||
grid.appendChild(colLeft);
|
||||
grid.appendChild(colRight);
|
||||
row.appendChild(grid);
|
||||
|
||||
// Add "Search Online" button
|
||||
const searchBtnWrap = document.createElement("div");
|
||||
searchBtnWrap.style.marginTop = "4px";
|
||||
const searchBtn = document.createElement("button");
|
||||
searchBtn.textContent = "🔍 Search Online";
|
||||
searchBtn.className = "mjr-ps-btn";
|
||||
searchBtn.style.padding = "4px 8px";
|
||||
searchBtn.style.borderRadius = "6px";
|
||||
searchBtn.style.border = "1px solid rgba(46, 136, 255, 0.4)";
|
||||
searchBtn.style.background = "rgba(46, 136, 255, 0.15)";
|
||||
searchBtn.style.color = "#eee";
|
||||
searchBtn.style.cursor = "pointer";
|
||||
searchBtn.style.fontSize = "11px";
|
||||
searchBtn.style.width = "100%";
|
||||
|
||||
searchBtn.addEventListener("click", async () => {
|
||||
// Extract model name from missing_value or key
|
||||
const searchQuery = (entry?.key || entry?.missing_value || "")
|
||||
.replace(/\.(safetensors|ckpt|pt|pth|bin)$/i, "");
|
||||
|
||||
if (!searchQuery) {
|
||||
alert("Cannot determine model name to search for");
|
||||
return;
|
||||
}
|
||||
|
||||
searchBtn.disabled = true;
|
||||
searchBtn.textContent = "⏳ Searching...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/mjr_models/search_online", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: searchQuery, limit: 3 })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.ok) {
|
||||
alert("Search failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const results = data.results;
|
||||
// Use sorted_results if available (sorted by match score)
|
||||
const allResults = results.sorted_results || [
|
||||
...(results.platforms?.civitai || []),
|
||||
...(results.platforms?.huggingface || []),
|
||||
...(results.platforms?.github || [])
|
||||
];
|
||||
|
||||
// Check if we should show "Search on Google" button
|
||||
const googleUrl = results.google_search_url;
|
||||
|
||||
if (allResults.length === 0) {
|
||||
const message = `No results found for "${searchQuery}"\n\nTry:\n- Simplifying the search query\n- Using fewer words\n- Checking the spelling\n\nOr search on Google?`;
|
||||
|
||||
if (confirm(message)) {
|
||||
// Open Google search in new tab
|
||||
if (googleUrl) {
|
||||
window.open(googleUrl, '_blank');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show results dialog (with Google button if few results)
|
||||
showSearchResultsDialog(allResults, (selectedResult) => {
|
||||
if (selectedResult) {
|
||||
urlField.value = selectedResult.url;
|
||||
if (selectedResult.filename) fileField.value = selectedResult.filename;
|
||||
if (selectedResult.sha256) shaField.value = selectedResult.sha256;
|
||||
if (selectedResult.type) setKindValue(selectedResult.type);
|
||||
downloadCb.checked = true;
|
||||
updateExistsHighlight();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Search error:", error);
|
||||
alert("Search error: " + error.message);
|
||||
} finally {
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.textContent = "🔍 Search Online";
|
||||
}
|
||||
});
|
||||
|
||||
searchBtnWrap.appendChild(searchBtn);
|
||||
row.appendChild(searchBtnWrap);
|
||||
|
||||
if (recipe) {
|
||||
const recipeLine = document.createElement("div");
|
||||
recipeLine.textContent = formatRecipeText(recipe);
|
||||
recipeLine.style.opacity = "0.75";
|
||||
recipeLine.style.fontSize = "10px";
|
||||
recipeLine.style.wordBreak = "break-word";
|
||||
row.appendChild(recipeLine);
|
||||
}
|
||||
|
||||
if (recipe?.url) {
|
||||
urlField.value = recipe.url;
|
||||
}
|
||||
if (recipe?.filename) {
|
||||
fileField.value = recipe.filename;
|
||||
}
|
||||
if (recipe?.sha256) {
|
||||
shaField.value = recipe.sha256;
|
||||
}
|
||||
const setKindValue = (value) => {
|
||||
if (!value) return false;
|
||||
const exists = Array.from(kindField.options).some((opt) => opt.value === value);
|
||||
if (exists) {
|
||||
kindField.value = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!setKindValue(recipe?.kind)) {
|
||||
setKindValue(entry?.kind);
|
||||
}
|
||||
|
||||
let locked = isPrefilled;
|
||||
const setLocked = (value) => {
|
||||
locked = value;
|
||||
urlField.readOnly = locked;
|
||||
fileField.readOnly = locked;
|
||||
shaField.readOnly = locked;
|
||||
kindField.disabled = locked;
|
||||
};
|
||||
setLocked(isPrefilled);
|
||||
|
||||
if (isNoteRecipe) {
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.textContent = "Edit";
|
||||
editBtn.className = "mjr-ps-btn";
|
||||
editBtn.style.padding = "2px 6px";
|
||||
editBtn.style.borderRadius = "6px";
|
||||
editBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
editBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
editBtn.style.color = "#eee";
|
||||
editBtn.style.cursor = "pointer";
|
||||
editBtn.addEventListener("click", () => {
|
||||
setLocked(!locked);
|
||||
editBtn.textContent = locked ? "Edit" : "Lock";
|
||||
});
|
||||
titleWrap.appendChild(editBtn);
|
||||
}
|
||||
|
||||
const normalizeName = (value) => String(value || "").replace(/\\/g, "/").toLowerCase();
|
||||
const existsFor = (kind, filename) => {
|
||||
if (!existing || !kind || !filename) return false;
|
||||
const set = existing.get(kind);
|
||||
if (!set) return false;
|
||||
return set.has(normalizeName(filename));
|
||||
};
|
||||
|
||||
const updateExistsHighlight = () => {
|
||||
const kind = String(kindField.value || "").trim();
|
||||
const filename = String(fileField.value || extractFilenameFromUrl(urlField.value || "") || "").trim();
|
||||
if (!filename) {
|
||||
row.style.border = defaultBorder;
|
||||
row.style.background = defaultBackground;
|
||||
return;
|
||||
}
|
||||
if (existsFor(kind, filename)) {
|
||||
row.style.border = "1px solid rgba(47, 143, 78, 0.7)";
|
||||
row.style.background = "rgba(47, 143, 78, 0.12)";
|
||||
} else {
|
||||
row.style.border = defaultBorder;
|
||||
row.style.background = defaultBackground;
|
||||
}
|
||||
};
|
||||
|
||||
urlField.addEventListener("input", updateExistsHighlight);
|
||||
fileField.addEventListener("input", updateExistsHighlight);
|
||||
kindField.addEventListener("change", updateExistsHighlight);
|
||||
updateExistsHighlight();
|
||||
|
||||
list.appendChild(row);
|
||||
rows.push({
|
||||
entry,
|
||||
downloadCb,
|
||||
urlInput: urlField,
|
||||
kindSelect: kindField,
|
||||
filenameInput: fileField,
|
||||
shaInput: shaField,
|
||||
});
|
||||
}
|
||||
|
||||
const rememberWrap = document.createElement("label");
|
||||
rememberWrap.style.display = "flex";
|
||||
rememberWrap.style.alignItems = "center";
|
||||
rememberWrap.style.gap = "6px";
|
||||
rememberWrap.style.cursor = "pointer";
|
||||
const rememberCb = document.createElement("input");
|
||||
rememberCb.type = "checkbox";
|
||||
rememberCb.checked = true;
|
||||
const rememberText = document.createElement("span");
|
||||
rememberText.textContent = "Remember these sources";
|
||||
rememberText.style.opacity = "0.85";
|
||||
rememberWrap.appendChild(rememberCb);
|
||||
rememberWrap.appendChild(rememberText);
|
||||
panel.appendChild(rememberWrap);
|
||||
|
||||
const errorText = document.createElement("div");
|
||||
errorText.style.color = "#f2a5a5";
|
||||
errorText.style.fontSize = "11px";
|
||||
errorText.style.minHeight = "14px";
|
||||
panel.appendChild(errorText);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "flex-end";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.className = "mjr-ps-btn";
|
||||
cancelBtn.style.padding = "6px 10px";
|
||||
cancelBtn.style.borderRadius = "8px";
|
||||
cancelBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
cancelBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
cancelBtn.style.color = "#eee";
|
||||
cancelBtn.style.cursor = "pointer";
|
||||
|
||||
const downloadBtn = document.createElement("button");
|
||||
downloadBtn.textContent = "Download Selected";
|
||||
downloadBtn.className = "mjr-ps-btn";
|
||||
downloadBtn.style.padding = "6px 10px";
|
||||
downloadBtn.style.borderRadius = "8px";
|
||||
downloadBtn.style.border = "1px solid rgba(46, 136, 255, 0.65)";
|
||||
downloadBtn.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
downloadBtn.style.color = "#eee";
|
||||
downloadBtn.style.cursor = "pointer";
|
||||
|
||||
actions.appendChild(cancelBtn);
|
||||
actions.appendChild(downloadBtn);
|
||||
panel.appendChild(actions);
|
||||
|
||||
const close = (value) => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
if (overlay.parentNode) overlay.remove();
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (String(event.key || "").toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
close(null);
|
||||
}
|
||||
};
|
||||
|
||||
downloadBtn.addEventListener("click", () => {
|
||||
errorText.textContent = "";
|
||||
const downloadItems = [];
|
||||
const saveItems = [];
|
||||
for (const row of rows) {
|
||||
if (!row.downloadCb.checked) continue;
|
||||
const entry = row.entry || {};
|
||||
const url = String(row.urlInput?.value || "").trim();
|
||||
const kind = String(row.kindSelect?.value || "").trim();
|
||||
const filenameRaw = String(row.filenameInput?.value || "").trim();
|
||||
const shaRaw = String(row.shaInput?.value || "").trim().toLowerCase();
|
||||
|
||||
if (!url || !isValidUrl(url)) {
|
||||
errorText.textContent = "Please provide a valid http/https URL.";
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = filenameRaw || extractFilenameFromUrl(url);
|
||||
if (!filename || !hasAllowedExtension(filename)) {
|
||||
errorText.textContent = "Filename must use a supported extension.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kind) {
|
||||
errorText.textContent = "Please select a valid kind.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (shaRaw && !/^[0-9a-f]{64}$/.test(shaRaw)) {
|
||||
errorText.textContent = "SHA256 must be 64 hex characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
const item = {
|
||||
key: entry.key,
|
||||
kind,
|
||||
url,
|
||||
filename,
|
||||
sha256: shaRaw || null,
|
||||
};
|
||||
downloadItems.push(item);
|
||||
if (rememberCb.checked) {
|
||||
saveItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!downloadItems.length) {
|
||||
errorText.textContent = "Select at least one item to download.";
|
||||
return;
|
||||
}
|
||||
|
||||
close({
|
||||
items: downloadItems,
|
||||
saveItems,
|
||||
remember: rememberCb.checked,
|
||||
});
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => close(null));
|
||||
overlay.addEventListener("click", (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import { ensureStyles } from "./toast.js";
|
||||
|
||||
function buildResultMap(results) {
|
||||
const map = new Map();
|
||||
for (const entry of results || []) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
map.set(key, {
|
||||
candidates: Array.isArray(entry?.candidates) ? entry.candidates : [],
|
||||
exact_match_wrong_folder: entry?.exact_match_wrong_folder || null,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function formatCandidateLabel(candidate) {
|
||||
const score = Number(candidate?.score || 0);
|
||||
const basename = String(candidate?.basename || candidate?.relpath || "");
|
||||
const kind = String(candidate?.kind || "");
|
||||
if (kind) {
|
||||
return `${score} | ${basename} (${kind})`;
|
||||
}
|
||||
return `${score} | ${basename}`;
|
||||
}
|
||||
|
||||
export function showModelFixerDialog({ missing, results } = {}) {
|
||||
ensureStyles();
|
||||
const missingList = Array.isArray(missing) ? missing : [];
|
||||
const resultMap = buildResultMap(results);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.inset = "0";
|
||||
overlay.style.background = "rgba(5, 6, 10, 0.75)";
|
||||
overlay.style.zIndex = "10010";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.padding = "16px";
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.style.width = "min(640px, 92vw)";
|
||||
panel.style.maxHeight = "80vh";
|
||||
panel.style.overflow = "auto";
|
||||
panel.style.background = "rgba(18, 20, 26, 0.95)";
|
||||
panel.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.padding = "14px 16px";
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.gap = "10px";
|
||||
panel.style.color = "#eee";
|
||||
panel.style.fontSize = "12px";
|
||||
panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.4)";
|
||||
overlay.appendChild(panel);
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.textContent = "Fix Missing Models";
|
||||
title.style.fontWeight = "600";
|
||||
title.style.fontSize = "13px";
|
||||
title.style.letterSpacing = "0.4px";
|
||||
panel.appendChild(title);
|
||||
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.textContent = `Missing entries: ${missingList.length}`;
|
||||
subtitle.style.opacity = "0.8";
|
||||
subtitle.style.fontSize = "11px";
|
||||
panel.appendChild(subtitle);
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.style.display = "flex";
|
||||
list.style.flexDirection = "column";
|
||||
list.style.gap = "8px";
|
||||
panel.appendChild(list);
|
||||
|
||||
const rows = [];
|
||||
for (const entry of missingList) {
|
||||
const missingValue = String(entry?.missing_value || "");
|
||||
const typeHint = String(entry?.type_hint || "unknown");
|
||||
const key = `${missingValue}::${typeHint}`;
|
||||
const resultData = resultMap.get(key) || { candidates: [], exact_match_wrong_folder: null };
|
||||
const candidates = resultData.candidates || [];
|
||||
const wrongFolderMatch = resultData.exact_match_wrong_folder;
|
||||
const exactMatches = candidates.filter((c) => Number(c?.score || 0) === 100);
|
||||
const exactUnique = exactMatches.length === 1;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.style.display = "flex";
|
||||
row.style.flexDirection = "column";
|
||||
row.style.gap = "6px";
|
||||
row.style.padding = "8px";
|
||||
row.style.borderRadius = "8px";
|
||||
row.style.border = "1px solid rgba(255,255,255,0.08)";
|
||||
row.style.background = "rgba(12, 14, 18, 0.6)";
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.style.display = "flex";
|
||||
label.style.alignItems = "center";
|
||||
label.style.gap = "6px";
|
||||
const titleText = document.createElement("div");
|
||||
const nodeLabel = entry?.node_title || `Node ${entry?.node_id ?? "?"}`;
|
||||
const widgetLabel = entry?.widget_name || "widget";
|
||||
titleText.textContent = `${nodeLabel} - ${widgetLabel}`;
|
||||
titleText.style.fontWeight = "600";
|
||||
const autoTag = document.createElement("span");
|
||||
if (exactUnique) {
|
||||
autoTag.textContent = "auto";
|
||||
autoTag.style.fontSize = "10px";
|
||||
autoTag.style.padding = "1px 6px";
|
||||
autoTag.style.borderRadius = "999px";
|
||||
autoTag.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
autoTag.style.border = "1px solid rgba(46, 136, 255, 0.45)";
|
||||
autoTag.style.color = "#cfe3ff";
|
||||
}
|
||||
label.appendChild(titleText);
|
||||
if (exactUnique) {
|
||||
label.appendChild(autoTag);
|
||||
}
|
||||
|
||||
const missingLine = document.createElement("div");
|
||||
missingLine.textContent = `Missing: ${missingValue}`;
|
||||
missingLine.style.opacity = "0.8";
|
||||
missingLine.style.fontSize = "11px";
|
||||
|
||||
// Show "Move to correct folder" button if exact match found in wrong folder
|
||||
let moveButton = null;
|
||||
if (wrongFolderMatch) {
|
||||
const moveContainer = document.createElement("div");
|
||||
moveContainer.style.display = "flex";
|
||||
moveContainer.style.alignItems = "center";
|
||||
moveContainer.style.gap = "8px";
|
||||
moveContainer.style.padding = "6px 8px";
|
||||
moveContainer.style.borderRadius = "8px";
|
||||
moveContainer.style.background = "rgba(255, 165, 0, 0.15)";
|
||||
moveContainer.style.border = "1px solid rgba(255, 165, 0, 0.3)";
|
||||
|
||||
const moveInfo = document.createElement("div");
|
||||
moveInfo.style.flex = "1";
|
||||
moveInfo.style.fontSize = "11px";
|
||||
moveInfo.style.color = "#ffb74d";
|
||||
moveInfo.textContent = `Found in ${wrongFolderMatch.kind}/ → Move to ${wrongFolderMatch.expected_kind}/`;
|
||||
|
||||
moveButton = document.createElement("button");
|
||||
moveButton.textContent = "Move";
|
||||
moveButton.className = "mjr-ps-btn";
|
||||
moveButton.style.padding = "4px 10px";
|
||||
moveButton.style.borderRadius = "6px";
|
||||
moveButton.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
moveButton.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
moveButton.style.color = "#ffcc80";
|
||||
moveButton.style.cursor = "pointer";
|
||||
moveButton.style.fontSize = "11px";
|
||||
moveButton.dataset.sourceKind = wrongFolderMatch.kind;
|
||||
moveButton.dataset.sourceRelpath = wrongFolderMatch.relpath;
|
||||
moveButton.dataset.targetKind = wrongFolderMatch.expected_kind;
|
||||
|
||||
moveContainer.appendChild(moveInfo);
|
||||
moveContainer.appendChild(moveButton);
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
row.appendChild(moveContainer);
|
||||
} else {
|
||||
row.appendChild(label);
|
||||
row.appendChild(missingLine);
|
||||
}
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.className = "mjr-ps-select";
|
||||
select.style.width = "100%";
|
||||
select.style.padding = "6px 8px";
|
||||
select.style.borderRadius = "8px";
|
||||
select.style.border = "1px solid rgba(255,255,255,0.12)";
|
||||
select.style.color = "#f2f4f8";
|
||||
|
||||
const emptyOpt = document.createElement("option");
|
||||
emptyOpt.value = "";
|
||||
emptyOpt.textContent = candidates.length ? "-- Select match --" : "No candidates found";
|
||||
select.appendChild(emptyOpt);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(candidate?.relpath || "");
|
||||
opt.textContent = formatCandidateLabel(candidate);
|
||||
opt.title = String(candidate?.relpath || "");
|
||||
select.appendChild(opt);
|
||||
}
|
||||
if (exactUnique) {
|
||||
select.value = String(exactMatches[0]?.relpath || "");
|
||||
}
|
||||
if (!candidates.length) {
|
||||
select.disabled = true;
|
||||
select.style.opacity = "0.6";
|
||||
}
|
||||
|
||||
row.appendChild(select);
|
||||
list.appendChild(row);
|
||||
|
||||
rows.push({
|
||||
node_id: entry?.node_id,
|
||||
widget_name: entry?.widget_name,
|
||||
select,
|
||||
moveButton,
|
||||
});
|
||||
}
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.style.display = "flex";
|
||||
actions.style.gap = "8px";
|
||||
actions.style.justifyContent = "flex-end";
|
||||
actions.style.marginTop = "6px";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.className = "mjr-ps-btn";
|
||||
cancelBtn.style.padding = "6px 10px";
|
||||
cancelBtn.style.borderRadius = "8px";
|
||||
cancelBtn.style.border = "1px solid rgba(255,255,255,0.2)";
|
||||
cancelBtn.style.background = "rgba(255,255,255,0.06)";
|
||||
cancelBtn.style.color = "#eee";
|
||||
cancelBtn.style.cursor = "pointer";
|
||||
|
||||
const applyBtn = document.createElement("button");
|
||||
applyBtn.textContent = "Apply Fix";
|
||||
applyBtn.className = "mjr-ps-btn";
|
||||
applyBtn.style.padding = "6px 10px";
|
||||
applyBtn.style.borderRadius = "8px";
|
||||
applyBtn.style.border = "1px solid rgba(46, 136, 255, 0.65)";
|
||||
applyBtn.style.background = "rgba(46, 136, 255, 0.25)";
|
||||
applyBtn.style.color = "#eee";
|
||||
applyBtn.style.cursor = "pointer";
|
||||
|
||||
actions.appendChild(cancelBtn);
|
||||
actions.appendChild(applyBtn);
|
||||
panel.appendChild(actions);
|
||||
|
||||
const close = (value) => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
if (overlay.parentNode) overlay.remove();
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (String(event.key || "").toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
close(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listeners to all move buttons
|
||||
for (const row of rows) {
|
||||
if (row.moveButton) {
|
||||
row.moveButton.addEventListener("click", async () => {
|
||||
const button = row.moveButton;
|
||||
const sourceKind = button.dataset.sourceKind;
|
||||
const sourceRelpath = button.dataset.sourceRelpath;
|
||||
const targetKind = button.dataset.targetKind;
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = "Moving...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/mjr_models/move_to_correct_folder", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
source_kind: sourceKind,
|
||||
source_relpath: sourceRelpath,
|
||||
target_kind: targetKind,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok) {
|
||||
button.textContent = "Moved!";
|
||||
button.style.background = "rgba(76, 175, 80, 0.25)";
|
||||
button.style.border = "1px solid rgba(76, 175, 80, 0.5)";
|
||||
button.style.color = "#a5d6a7";
|
||||
|
||||
// Auto-select the newly moved file in the dropdown
|
||||
if (row.select && data.target_relpath) {
|
||||
row.select.value = data.target_relpath;
|
||||
}
|
||||
|
||||
// Refresh the folder to show the new file
|
||||
setTimeout(() => {
|
||||
if (typeof app?.refreshComboInNodes === "function") {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
throw new Error(data.error || "Move failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[mjr] Failed to move model:", error);
|
||||
button.textContent = "Error";
|
||||
button.style.background = "rgba(244, 67, 54, 0.25)";
|
||||
button.style.border = "1px solid rgba(244, 67, 54, 0.5)";
|
||||
button.disabled = false;
|
||||
setTimeout(() => {
|
||||
button.textContent = "Move";
|
||||
button.style.background = "rgba(255, 165, 0, 0.25)";
|
||||
button.style.border = "1px solid rgba(255, 165, 0, 0.5)";
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const fixes = [];
|
||||
for (const row of rows) {
|
||||
const value = row.select.value;
|
||||
if (!value) continue;
|
||||
fixes.push({
|
||||
node_id: row.node_id,
|
||||
widget_name: row.widget_name,
|
||||
new_value: value,
|
||||
});
|
||||
}
|
||||
close(fixes);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener("click", () => close(null));
|
||||
overlay.addEventListener("click", (event) => {
|
||||
if (event.target === overlay) {
|
||||
close(null);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
export function titlePathJS(text) {
|
||||
if (text == null) return "";
|
||||
let t = String(text).trim();
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/[\\/]/g, " ");
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean);
|
||||
const titled = tokens.map((tok) => {
|
||||
if (tok.length <= 1) return tok.toUpperCase();
|
||||
return tok[0].toUpperCase() + tok.slice(1).toLowerCase();
|
||||
});
|
||||
return titled.join("_") || "Project";
|
||||
}
|
||||
|
||||
export function token3Tag(raw, upper) {
|
||||
if (raw == null) return "";
|
||||
let t = String(raw).trim();
|
||||
if (!t) return "";
|
||||
if (t.normalize) {
|
||||
t = t.normalize("NFKD").replace(/[^\x00-\x7F]/g, "");
|
||||
}
|
||||
t = t.replace(/\\/g, "/").split("/").pop();
|
||||
const lastDot = t.lastIndexOf(".");
|
||||
if (lastDot > 0) {
|
||||
t = t.slice(0, lastDot);
|
||||
}
|
||||
const tokens = t.split(/[\s_-]+/).filter(Boolean).slice(0, 3);
|
||||
if (!tokens.length) return "";
|
||||
if (upper) {
|
||||
return tokens.join("_").toUpperCase();
|
||||
}
|
||||
return tokens
|
||||
.map((tok) =>
|
||||
tok.length <= 1 ? tok.toUpperCase() : tok[0].toUpperCase() + tok.slice(1).toLowerCase()
|
||||
)
|
||||
.join("_");
|
||||
}
|
||||
|
||||
export function yymmddJS() {
|
||||
const d = new Date();
|
||||
const yy = String(d.getFullYear()).slice(-2);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${yy}${mm}${dd}`;
|
||||
}
|
||||
|
||||
export function safeRel(rel) {
|
||||
if (rel == null) return "";
|
||||
let r = String(rel).replace(/\\/g, "/").trim();
|
||||
r = r.replace(/^output\//i, "");
|
||||
r = r.replace(/^\/+/, "");
|
||||
r = r
|
||||
.split("/")
|
||||
.filter((p) => p && p !== "." && p !== "..")
|
||||
.join("/");
|
||||
return r;
|
||||
}
|
||||
|
||||
export function joinRel(a, b) {
|
||||
const left = safeRel(a);
|
||||
const right = safeRel(b);
|
||||
if (!left) return right;
|
||||
if (!right) return left;
|
||||
return `${left.replace(/\/+$/, "")}/${right.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
export function mediaDir(media) {
|
||||
const m = String(media || "").toLowerCase();
|
||||
if (m === "videos") return "02_OUT/VIDEOS";
|
||||
if (m === "images") return "02_OUT/IMAGES";
|
||||
return "02_OUT/OTHER";
|
||||
}
|
||||
|
||||
export function makeKindToken(kind) {
|
||||
const k = String(kind || "").toLowerCase();
|
||||
if (k === "asset") return "ASSET";
|
||||
if (k === "shot") return "SHOT";
|
||||
return "MISC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a path is relative and safe.
|
||||
*/
|
||||
function validateRelativePath(path, context) {
|
||||
if (path.startsWith("/") || path.startsWith("\\")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes(":")) {
|
||||
throw new Error(`${context} must be relative`);
|
||||
}
|
||||
if (path.includes("..")) {
|
||||
throw new Error(`${context} contains '..'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTemplatePreview(template, tokens) {
|
||||
if (!template) return "";
|
||||
let out = template.replace(/\\/g, "/").trim();
|
||||
if (!out) return "";
|
||||
|
||||
// Validate template before processing
|
||||
validateRelativePath(out, "template");
|
||||
|
||||
if (!out.includes("{BASE}")) {
|
||||
throw new Error("template must include {BASE}");
|
||||
}
|
||||
|
||||
// Replace tokens (use split/join to handle multiple occurrences safely)
|
||||
for (const [key, value] of Object.entries(tokens)) {
|
||||
const safeValue = String(value || "");
|
||||
// Prevent token values from containing other token patterns
|
||||
if (safeValue.includes("{") || safeValue.includes("}")) {
|
||||
throw new Error(`token value for {${key}} contains template markers`);
|
||||
}
|
||||
out = out.split(`{${key}}`).join(safeValue);
|
||||
}
|
||||
|
||||
// Normalize slashes
|
||||
out = out.replace(/\\/g, "/").replace(/\/+/g, "/");
|
||||
|
||||
// Validate resolved path
|
||||
validateRelativePath(out, "resolved path");
|
||||
|
||||
const parts = out.split("/").filter(Boolean);
|
||||
|
||||
// Verify base constraint
|
||||
const base = String(tokens?.BASE || "").replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
if (base) {
|
||||
if (!(out === base || out.startsWith(`${base}/`))) {
|
||||
throw new Error("template must stay under {BASE}");
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("/");
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
const NOTE_TYPE_RE = /markdown|note/i;
|
||||
const NOTE_TITLE_RE = /note/i;
|
||||
const ALLOWED_EXTENSIONS = [".safetensors", ".ckpt", ".pt", ".pth", ".bin"];
|
||||
const SECTION_KINDS = new Set([
|
||||
"text_encoders",
|
||||
"clip_vision",
|
||||
"loras",
|
||||
"diffusion_models",
|
||||
"vae",
|
||||
]);
|
||||
|
||||
function normalizeSectionLine(line) {
|
||||
let t = String(line || "").trim().toLowerCase();
|
||||
if (!t) return "";
|
||||
t = t.replace(/^#+\s*/, "");
|
||||
t = t.replace(/:\s*$/, "");
|
||||
return t.trim();
|
||||
}
|
||||
|
||||
function normalizeUrl(url) {
|
||||
let u = String(url || "").trim();
|
||||
if (!u) return "";
|
||||
u = u.replace(/\/blob\//i, "/resolve/");
|
||||
return u;
|
||||
}
|
||||
|
||||
function basenameFromUrl(url) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const path = parsed.pathname || "";
|
||||
const parts = path.split("/").filter(Boolean);
|
||||
const base = parts.length ? parts[parts.length - 1] : "";
|
||||
return base;
|
||||
} catch (_) {
|
||||
const clean = String(url || "").split("?")[0].split("#")[0];
|
||||
const parts = clean.split("/").filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : "";
|
||||
}
|
||||
}
|
||||
|
||||
function hasAllowedExtension(filename) {
|
||||
const lower = String(filename || "").toLowerCase();
|
||||
return ALLOWED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
function inferKindFromUrl(url) {
|
||||
const lower = String(url || "").toLowerCase();
|
||||
|
||||
// Check URL path for kind indicators
|
||||
if (lower.includes("/text_encoders/") || lower.includes("text_encoder")) return "text_encoders";
|
||||
if (lower.includes("/diffusion_models/") || lower.includes("diffusion_model")) return "diffusion_models";
|
||||
if (lower.includes("/clip_vision/")) return "clip_vision";
|
||||
if (lower.includes("/loras/") || lower.includes("/lora/")) return "loras";
|
||||
if (lower.includes("/vae/")) return "vae";
|
||||
if (lower.includes("/controlnet/")) return "controlnet";
|
||||
if (lower.includes("/upscale_models/") || lower.includes("/upscale/")) return "upscale_models";
|
||||
if (lower.includes("/clip/")) return "clip";
|
||||
if (lower.includes("/unet/")) return "unet";
|
||||
if (lower.includes("/embeddings/") || lower.includes("/embedding/")) return "embeddings";
|
||||
if (lower.includes("/checkpoints/") || lower.includes("/checkpoint/")) return "checkpoints";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getNoteTextFromNode(node) {
|
||||
const props = node?.properties || {};
|
||||
const candidates = [
|
||||
props?.text,
|
||||
props?.value,
|
||||
props?.markdown,
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === "string" && candidate.trim().length > 20) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
const widgetsValues = node?.widgets_values;
|
||||
if (Array.isArray(widgetsValues)) {
|
||||
const parts = widgetsValues.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20) return joined;
|
||||
}
|
||||
|
||||
const widgets = node?.widgets;
|
||||
if (Array.isArray(widgets)) {
|
||||
const parts = widgets
|
||||
.map((w) => w?.value)
|
||||
.filter((v) => typeof v === "string" && v.trim().length > 0);
|
||||
const joined = parts.join("\n");
|
||||
if (joined.trim().length > 20) return joined;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isNoteLikeNode(node) {
|
||||
const type = String(node?.type || node?.class_type || "").trim();
|
||||
const title = String(node?.title || "").trim();
|
||||
return NOTE_TYPE_RE.test(type) || NOTE_TITLE_RE.test(title);
|
||||
}
|
||||
|
||||
function collectGraphsFromNode(node) {
|
||||
return [
|
||||
node?.subgraph,
|
||||
node?._subgraph,
|
||||
node?.subgraph?.graph,
|
||||
node?.subgraph?.lgraph,
|
||||
node?.properties?.subgraph,
|
||||
node?.subgraph_instance,
|
||||
node?.subgraph_instance?.graph,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function walkGraph(graph, visitor, visited) {
|
||||
if (!graph || !Array.isArray(graph.nodes)) return;
|
||||
if (visited.has(graph)) return;
|
||||
visited.add(graph);
|
||||
for (const node of graph.nodes) {
|
||||
visitor(node);
|
||||
const children = collectGraphsFromNode(node);
|
||||
for (const child of children) {
|
||||
if (Array.isArray(child?.nodes)) {
|
||||
walkGraph(child, visitor, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractNoteTexts(workflowJson) {
|
||||
if (!workflowJson || !Array.isArray(workflowJson.nodes)) return [];
|
||||
const texts = new Set();
|
||||
const visited = new WeakSet();
|
||||
const graph = { nodes: workflowJson.nodes };
|
||||
|
||||
walkGraph(
|
||||
graph,
|
||||
(node) => {
|
||||
if (!isNoteLikeNode(node)) return;
|
||||
const text = getNoteTextFromNode(node);
|
||||
if (text && text.trim().length > 20) {
|
||||
texts.add(text.trim());
|
||||
}
|
||||
},
|
||||
visited
|
||||
);
|
||||
|
||||
return Array.from(texts);
|
||||
}
|
||||
|
||||
export function parseRecipesFromNoteText(text) {
|
||||
const recipes = new Map();
|
||||
const lines = String(text || "").split(/\r?\n/);
|
||||
let currentKind = "unknown";
|
||||
|
||||
for (const line of lines) {
|
||||
const normalized = normalizeSectionLine(line);
|
||||
if (SECTION_KINDS.has(normalized)) {
|
||||
currentKind = normalized;
|
||||
continue;
|
||||
}
|
||||
|
||||
const urls = new Set();
|
||||
const mdRegex = /\[[^\]]+\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
let match = null;
|
||||
while ((match = mdRegex.exec(line)) !== null) {
|
||||
if (match[1]) urls.add(match[1]);
|
||||
}
|
||||
const rawRegex = /(https?:\/\/[^\s]+)/g;
|
||||
while ((match = rawRegex.exec(line)) !== null) {
|
||||
if (match[1]) urls.add(match[1]);
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
if (!normalizedUrl) continue;
|
||||
const filename = basenameFromUrl(normalizedUrl);
|
||||
if (!filename || !hasAllowedExtension(filename)) continue;
|
||||
if (recipes.has(filename)) {
|
||||
console.debug("[mjr] workflow note recipe duplicate", filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use section kind if available, otherwise infer from URL
|
||||
let finalKind = currentKind;
|
||||
if (!finalKind || finalKind === "unknown") {
|
||||
finalKind = inferKindFromUrl(normalizedUrl);
|
||||
}
|
||||
|
||||
recipes.set(filename, {
|
||||
key: filename,
|
||||
kind: finalKind,
|
||||
url: normalizedUrl,
|
||||
filename,
|
||||
source: "workflow_note",
|
||||
});
|
||||
}
|
||||
}
|
||||
return recipes;
|
||||
}
|
||||
|
||||
export function collectRecipesFromWorkflowNotes(workflowJson) {
|
||||
const texts = extractNoteTexts(workflowJson);
|
||||
const merged = new Map();
|
||||
for (const text of texts) {
|
||||
const parsed = parseRecipesFromNoteText(text);
|
||||
for (const [key, recipe] of parsed.entries()) {
|
||||
if (!merged.has(key)) {
|
||||
merged.set(key, recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
const STORAGE_KEY = "mjr_project_settings_state";
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
projectName: "",
|
||||
workflowName: "",
|
||||
workflowAsset: "",
|
||||
workflowLastBase: "",
|
||||
workflowNextSuffix: 1,
|
||||
workflowPanelOpen: false,
|
||||
projectId: "",
|
||||
projectFolder: "",
|
||||
projectExists: null,
|
||||
autoPatch: true,
|
||||
autoPatchNewNodes: true,
|
||||
autoSwitchTrusted: false,
|
||||
autoHookSave: true,
|
||||
kind: "asset",
|
||||
media: "images",
|
||||
name: "",
|
||||
modelSelection: "",
|
||||
detectedModelTag: "",
|
||||
useCustomModel: false,
|
||||
customModelText: "",
|
||||
modelUpper: false,
|
||||
pendingProjectName: "",
|
||||
pendingSelectProjectId: "",
|
||||
lastRelDir: "",
|
||||
lastPrefix: "",
|
||||
pathTemplate: "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}",
|
||||
showMoreModels: false,
|
||||
lastError: "",
|
||||
detected: null,
|
||||
workflowHasSignature: false,
|
||||
workflowChecked: false,
|
||||
isGraphLoading: false,
|
||||
graphLoadToken: 0,
|
||||
graphIsEmpty: false,
|
||||
};
|
||||
|
||||
export let runtimeState = null;
|
||||
|
||||
export function setRuntimeState(state) {
|
||||
runtimeState = state;
|
||||
}
|
||||
|
||||
export function createRuntimeState(overrides) {
|
||||
return Object.assign({}, DEFAULT_STATE, overrides || {});
|
||||
}
|
||||
|
||||
export function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveState(state) {
|
||||
const data = {
|
||||
projectName: state.projectName,
|
||||
workflowName: state.workflowName,
|
||||
workflowAsset: state.workflowAsset,
|
||||
workflowLastBase: state.workflowLastBase,
|
||||
workflowNextSuffix: state.workflowNextSuffix,
|
||||
workflowPanelOpen: state.workflowPanelOpen,
|
||||
projectId: state.projectId,
|
||||
projectFolder: state.projectFolder,
|
||||
autoPatch: state.autoPatch,
|
||||
autoPatchNewNodes: state.autoPatchNewNodes,
|
||||
autoSwitchTrusted: state.autoSwitchTrusted,
|
||||
autoHookSave: state.autoHookSave,
|
||||
kind: state.kind,
|
||||
media: state.media,
|
||||
name: state.name,
|
||||
modelSelection: state.modelSelection,
|
||||
detectedModelTag: state.detectedModelTag,
|
||||
useCustomModel: state.useCustomModel,
|
||||
customModelText: state.customModelText,
|
||||
modelUpper: state.modelUpper,
|
||||
pendingProjectName: state.pendingProjectName,
|
||||
pendingSelectProjectId: state.pendingSelectProjectId,
|
||||
lastRelDir: state.lastRelDir,
|
||||
lastPrefix: state.lastPrefix,
|
||||
pathTemplate: state.pathTemplate,
|
||||
showMoreModels: state.showMoreModels,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
} catch (_) {}
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
declare module "../../scripts/app.js" {
|
||||
export const app: any;
|
||||
}
|
||||
|
||||
declare module "../../../scripts/app.js" {
|
||||
export const app: any;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
MJR_DEBUG?: boolean;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["DOM", "ES2022"],
|
||||
"rootDir": "src",
|
||||
"outDir": "js",
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noCheck": true,
|
||||
"noEmitOnError": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user