diff --git a/.gitignore b/.gitignore index d817961..abacec2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ htmlcov/ # Build dist/ build/ +node_modules/ *.egg-info/ CHANGELOG.md docs/AUDIT_REPORT.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5812dd0 --- /dev/null +++ b/AGENTS.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//`. +- 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. diff --git a/docs/COMFYUI_INTEGRATION_AUDIT.md b/docs/COMFYUI_INTEGRATION_AUDIT.md new file mode 100644 index 0000000..4d53b0e --- /dev/null +++ b/docs/COMFYUI_INTEGRATION_AUDIT.md @@ -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. diff --git a/docs/TYPESCRIPT_MIGRATION_PLAN.md b/docs/TYPESCRIPT_MIGRATION_PLAN.md new file mode 100644 index 0000000..af7eb24 --- /dev/null +++ b/docs/TYPESCRIPT_MIGRATION_PLAN.md @@ -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. diff --git a/js/majoor_project_settings.d.ts b/js/majoor_project_settings.d.ts new file mode 100644 index 0000000..1b0dc3d --- /dev/null +++ b/js/majoor_project_settings.d.ts @@ -0,0 +1,6 @@ +/** + * Majoor Project Settings (standalone) + * - Sidebar UI when available (fallback floating panel) + * - No nodes, no polling + */ +export {}; diff --git a/js/majoor_project_settings.js b/js/majoor_project_settings.js index ebe29ab..b58a9ce 100644 --- a/js/majoor_project_settings.js +++ b/js/majoor_project_settings.js @@ -1,42 +1,19 @@ -/** +/** * 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 { 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 { 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"; @@ -54,790 +31,759 @@ 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] : ""; + 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] : ""; + 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`); + 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; + 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; + 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; + 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?.ui?.settings?.settingsValues?.[id]; - return toBool(raw, 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"); + 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`); - } + 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"); } - } 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; + 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 || "", + 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 }), }); - 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.projectName = projectName; + state.projectId = resp.project_id; + state.projectFolder = resp.project_folder; + state.projectExists = true; state.lastError = ""; - - const fileLabel = resp.file || "workflow.json"; - const projectInfo = resp.project_rel_path || "PROJECTS//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); + 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); - - 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); - } + state._ui?.refreshProjectsList?.(true); } - -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 - ); -} - -function registerExtensionSettings() { - const settings = app?.ui?.settings; - if (!settings?.addSetting) return; - try { - const values = settings.settingsValues || {}; - settings.addSetting({ - id: DOWNLOAD_MAX_GB_SETTING, - name: "MajoorPS: Download max size (GB)", - type: "number", - defaultValue: toNumber(values[DOWNLOAD_MAX_GB_SETTING], DEFAULT_DOWNLOAD_MAX_GB), - min: 1, - max: 1024, - step: 1, - category: SETTINGS_CATEGORY, - }); - settings.addSetting({ - id: AUTO_FIX_AFTER_DOWNLOAD_SETTING, - name: "MajoorPS: Auto-run Fix after download", - type: "boolean", - defaultValue: toBool(values[AUTO_FIX_AFTER_DOWNLOAD_SETTING], DEFAULT_AUTO_FIX_AFTER_DOWNLOAD), - category: SETTINGS_CATEGORY, - }); - settings.addSetting({ - id: AUTO_OPEN_PANEL_ON_MISSING_SETTING, - name: "MajoorPS: Auto-open panel on missing models", - type: "boolean", - defaultValue: toBool(values[AUTO_OPEN_PANEL_ON_MISSING_SETTING], DEFAULT_AUTO_OPEN_PANEL), - category: SETTINGS_CATEGORY, - }); - settings.addSetting({ - id: AUTO_SCAN_MISSING_ON_OPEN_SETTING, - name: "MajoorPS: Auto-scan missing on panel open", - type: "boolean", - defaultValue: toBool(values[AUTO_SCAN_MISSING_ON_OPEN_SETTING], DEFAULT_AUTO_SCAN_MISSING_ON_OPEN), - category: SETTINGS_CATEGORY, - }); - settings.addSetting({ - id: AUTO_REMEMBER_NOTE_SOURCES_SETTING, - name: "MajoorPS: Auto-remember note sources", - type: "boolean", - defaultValue: toBool(values[AUTO_REMEMBER_NOTE_SOURCES_SETTING], DEFAULT_AUTO_REMEMBER_NOTE_SOURCES), - category: SETTINGS_CATEGORY, - }); - settings.addSetting({ - id: NO_PROJECT_STATUS_BLACK_SETTING, - name: "MajoorPS: Black status when no project", - type: "boolean", - defaultValue: toBool(values[NO_PROJECT_STATUS_BLACK_SETTING], DEFAULT_NO_PROJECT_STATUS_BLACK), - category: SETTINGS_CATEGORY, - }); - } catch (e) { - console.warn("[mjr] settings registration failed", e); - } -} - -app.registerExtension({ - name: "Majoor.ProjectSettings", - - async setup() { - await loadConfig(); - registerExtensionSettings(); - 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); +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; } - } - - stampGraphProjectSignature(app, runtimeState); + return false; } - }, - - async nodeCreated(node) { - const state = runtimeState; - if (!state || state.isGraphLoading) return; - if (state.graphIsEmpty) { - if (handleGraphEmptyTransition(state, state.graphLoadToken, 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//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); + } } - 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; - } + 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; - } - - 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); - } + 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; - } - - if (filenameWidget && alreadyProjectPathed(filenameWidget.value)) { - if (!warnedPathed) { - warnedPathed = true; - toast("warn", "Patch skipped", "Node already points to a project path."); - } + const settings = app?.ui?.settings; + if (!settings?.addSetting) return; - } - if (pathWidget && alreadyProjectPathed(pathWidget.value)) { - if (!warnedPathed) { - warnedPathed = true; - toast("warn", "Patch skipped", "Node already points to a project path."); + 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 }); } - 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); - }, + } + 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); + }, }); +//# sourceMappingURL=majoor_project_settings.js.map \ No newline at end of file diff --git a/js/majoor_project_settings.js.map b/js/majoor_project_settings.js.map new file mode 100644 index 0000000..bb0d676 --- /dev/null +++ b/js/majoor_project_settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"majoor_project_settings.js","sourceRoot":"","sources":["../src/majoor_project_settings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/api.d.ts b/js/mjr/api.d.ts new file mode 100644 index 0000000..4361ea8 --- /dev/null +++ b/js/mjr/api.d.ts @@ -0,0 +1,19 @@ +export declare function fetchJSON(url: any, opts: any): Promise; +export declare function tryResolveProjectId(folder: any): Promise; +export declare function listProjects(): Promise; +export declare function setProject(project_name: any, create_base?: boolean): Promise; +export declare function getModels(): Promise; +export declare function createCustomOut(payload: any): Promise; +export declare function previewTemplate(template: any, tokens: any): Promise; +export declare function listExistingNames(project_id: any, media?: string): Promise; +export declare function saveWorkflow(payload: any): Promise; +export declare function scanModelCandidates(missing: any): Promise; +export declare function resolveModelRecipes(missing: any): Promise; +export declare function saveModelRecipes(items: any): Promise; +export declare function downloadModels(items: any): Promise; +export declare function getDownloadStatus(job_id: any): Promise; +export declare function getFingerprintCacheStatus(): Promise; +export declare function buildFingerprintCache(payload: any): Promise; +export declare function resolveFingerprint(payload: any): Promise; +export declare function searchRegistry(query: any, limit?: number): Promise; +export declare function contributeRegistry(payload: any): Promise; diff --git a/js/mjr/api.js b/js/mjr/api.js index b889b59..6d95aa2 100644 --- a/js/mjr/api.js +++ b/js/mjr/api.js @@ -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 \ No newline at end of file diff --git a/js/mjr/api.js.map b/js/mjr/api.js.map new file mode 100644 index 0000000..d480409 --- /dev/null +++ b/js/mjr/api.js.map @@ -0,0 +1 @@ +{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/mjr/api.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/dialog.d.ts b/js/mjr/dialog.d.ts new file mode 100644 index 0000000..1dec9ad --- /dev/null +++ b/js/mjr/dialog.d.ts @@ -0,0 +1,2 @@ +export declare function psPrompt({ title, message, defaultValue }?: {}): Promise; +export declare function psConfirm({ title, message }?: {}): Promise; diff --git a/js/mjr/dialog.js b/js/mjr/dialog.js index e9fdb0d..cbde989 100644 --- a/js/mjr/dialog.js +++ b/js/mjr/dialog.js @@ -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 \ No newline at end of file diff --git a/js/mjr/dialog.js.map b/js/mjr/dialog.js.map new file mode 100644 index 0000000..c0e200c --- /dev/null +++ b/js/mjr/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"} \ No newline at end of file diff --git a/js/mjr/graph.d.ts b/js/mjr/graph.d.ts new file mode 100644 index 0000000..63d0b73 --- /dev/null +++ b/js/mjr/graph.d.ts @@ -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; diff --git a/js/mjr/graph.js b/js/mjr/graph.js index d19c26f..0a57f28 100644 --- a/js/mjr/graph.js +++ b/js/mjr/graph.js @@ -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 \ No newline at end of file diff --git a/js/mjr/graph.js.map b/js/mjr/graph.js.map new file mode 100644 index 0000000..918666e --- /dev/null +++ b/js/mjr/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"} \ No newline at end of file diff --git a/js/mjr/log.d.ts b/js/mjr/log.d.ts new file mode 100644 index 0000000..6d42ae6 --- /dev/null +++ b/js/mjr/log.d.ts @@ -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; diff --git a/js/mjr/log.js b/js/mjr/log.js index bd11a28..7b2975b 100644 --- a/js/mjr/log.js +++ b/js/mjr/log.js @@ -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 \ No newline at end of file diff --git a/js/mjr/log.js.map b/js/mjr/log.js.map new file mode 100644 index 0000000..0185f36 --- /dev/null +++ b/js/mjr/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"} \ No newline at end of file diff --git a/js/mjr/model_contribution_dialog.d.ts b/js/mjr/model_contribution_dialog.d.ts new file mode 100644 index 0000000..c671fb6 --- /dev/null +++ b/js/mjr/model_contribution_dialog.d.ts @@ -0,0 +1 @@ +export declare function showModelContributionDialog(modelName?: string): void; diff --git a/js/mjr/model_contribution_dialog.js b/js/mjr/model_contribution_dialog.js index bc2d653..04d0b17 100644 --- a/js/mjr/model_contribution_dialog.js +++ b/js/mjr/model_contribution_dialog.js @@ -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 = `

Share a model URL

Help others by contributing a verified download link.

`; - - 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 \ No newline at end of file diff --git a/js/mjr/model_contribution_dialog.js.map b/js/mjr/model_contribution_dialog.js.map new file mode 100644 index 0000000..43b2556 --- /dev/null +++ b/js/mjr/model_contribution_dialog.js.map @@ -0,0 +1 @@ +{"version":3,"file":"model_contribution_dialog.js","sourceRoot":"","sources":["../../src/mjr/model_contribution_dialog.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/model_downloader.d.ts b/js/mjr/model_downloader.d.ts new file mode 100644 index 0000000..8553d83 --- /dev/null +++ b/js/mjr/model_downloader.d.ts @@ -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; diff --git a/js/mjr/model_downloader.js b/js/mjr/model_downloader.js index 6136669..a305cdd 100644 --- a/js/mjr/model_downloader.js +++ b/js/mjr/model_downloader.js @@ -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 \ No newline at end of file diff --git a/js/mjr/model_downloader.js.map b/js/mjr/model_downloader.js.map new file mode 100644 index 0000000..a2eba35 --- /dev/null +++ b/js/mjr/model_downloader.js.map @@ -0,0 +1 @@ +{"version":3,"file":"model_downloader.js","sourceRoot":"","sources":["../../src/mjr/model_downloader.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/model_fixer.d.ts b/js/mjr/model_fixer.d.ts new file mode 100644 index 0000000..79992d8 --- /dev/null +++ b/js/mjr/model_fixer.d.ts @@ -0,0 +1,6 @@ +export declare function scanMissingModelsFromGraph(): any[]; +export declare function scanMissingModelsWithStats(): { + missing: any[]; + total: number; +}; +export declare function applyFixesToGraph(fixes: any): number; diff --git a/js/mjr/model_fixer.js b/js/mjr/model_fixer.js index e9f9b57..f489720 100644 --- a/js/mjr/model_fixer.js +++ b/js/mjr/model_fixer.js @@ -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 \ No newline at end of file diff --git a/js/mjr/model_fixer.js.map b/js/mjr/model_fixer.js.map new file mode 100644 index 0000000..cd81c41 --- /dev/null +++ b/js/mjr/model_fixer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"model_fixer.js","sourceRoot":"","sources":["../../src/mjr/model_fixer.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/patch.d.ts b/js/mjr/patch.d.ts new file mode 100644 index 0000000..1394ed8 --- /dev/null +++ b/js/mjr/patch.d.ts @@ -0,0 +1,8 @@ +export declare let PATH_WIDGETS: any[]; +export declare function loadConfig(): Promise; +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; +export declare function stampGraphProjectSignature(app: any, state: any): void; diff --git a/js/mjr/patch.js b/js/mjr/patch.js index 3c4a52f..0481313 100644 --- a/js/mjr/patch.js +++ b/js/mjr/patch.js @@ -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 \ No newline at end of file diff --git a/js/mjr/patch.js.map b/js/mjr/patch.js.map new file mode 100644 index 0000000..b585295 --- /dev/null +++ b/js/mjr/patch.js.map @@ -0,0 +1 @@ +{"version":3,"file":"patch.js","sourceRoot":"","sources":["../../src/mjr/patch.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/toast.d.ts b/js/mjr/toast.d.ts new file mode 100644 index 0000000..b510b07 --- /dev/null +++ b/js/mjr/toast.d.ts @@ -0,0 +1,2 @@ +export declare function ensureStyles(): void; +export declare function toast(type: any, title: any, message: any, opts?: {}): void; diff --git a/js/mjr/toast.js b/js/mjr/toast.js index 45281d9..e214778 100644 --- a/js/mjr/toast.js +++ b/js/mjr/toast.js @@ -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 \ No newline at end of file diff --git a/js/mjr/toast.js.map b/js/mjr/toast.js.map new file mode 100644 index 0000000..543fc04 --- /dev/null +++ b/js/mjr/toast.js.map @@ -0,0 +1 @@ +{"version":3,"file":"toast.js","sourceRoot":"","sources":["../../src/mjr/toast.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/ui_model_downloader_dialog.d.ts b/js/mjr/ui_model_downloader_dialog.d.ts new file mode 100644 index 0000000..dd1c67a --- /dev/null +++ b/js/mjr/ui_model_downloader_dialog.d.ts @@ -0,0 +1 @@ +export declare function showModelDownloaderDialog({ entries, kindOptions, existingMap }?: {}): Promise; diff --git a/js/mjr/ui_model_downloader_dialog.js b/js/mjr/ui_model_downloader_dialog.js index 99291bd..64c2e3e 100644 --- a/js/mjr/ui_model_downloader_dialog.js +++ b/js/mjr/ui_model_downloader_dialog.js @@ -1,729 +1,661 @@ import { ensureStyles } from "./toast.js"; -import { - extractFilenameFromUrl, - getAllowedExtensions, - hasAllowedExtension, - isValidUrl, -} from "./model_downloader.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(" | "); + 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) => { + ensureStyles(); 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.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(720px, 92vw)"; - panel.style.maxHeight = "80vh"; + panel.style.width = "min(600px, 90vw)"; + panel.style.maxHeight = "70vh"; panel.style.overflow = "auto"; - panel.style.background = "rgba(18, 20, 26, 0.95)"; + 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 = "14px 16px"; + panel.style.padding = "16px"; panel.style.display = "flex"; panel.style.flexDirection = "column"; - panel.style.gap = "10px"; + panel.style.gap = "12px"; panel.style.color = "#eee"; panel.style.fontSize = "12px"; - panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.4)"; + panel.style.boxShadow = "0 12px 32px rgba(0,0,0,0.5)"; overlay.appendChild(panel); - const title = document.createElement("div"); - title.textContent = "Download Missing Models"; + title.textContent = `Search Results (${results.length})`; title.style.fontWeight = "600"; - title.style.fontSize = "13px"; + title.style.fontSize = "14px"; 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"; + 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)"; }); - 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; + 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"; } - 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; + 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"; } - }; - - 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, - }); + 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); } - - 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"; - + panel.appendChild(resultsList); const cancelBtn = document.createElement("button"); cancelBtn.textContent = "Cancel"; cancelBtn.className = "mjr-ps-btn"; - cancelBtn.style.padding = "6px 10px"; + 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"; - - 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.style.alignSelf = "flex-end"; + cancelBtn.addEventListener("click", () => { + overlay.remove(); + callback(null); }); - - cancelBtn.addEventListener("click", () => close(null)); - overlay.addEventListener("click", (event) => { - if (event.target === overlay) { - close(null); - } + panel.appendChild(cancelBtn); + overlay.addEventListener("click", (e) => { + if (e.target === overlay) { + overlay.remove(); + callback(null); + } }); - document.addEventListener("keydown", onKeyDown); 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); + }); +} +//# sourceMappingURL=ui_model_downloader_dialog.js.map \ No newline at end of file diff --git a/js/mjr/ui_model_downloader_dialog.js.map b/js/mjr/ui_model_downloader_dialog.js.map new file mode 100644 index 0000000..9413bf8 --- /dev/null +++ b/js/mjr/ui_model_downloader_dialog.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ui_model_downloader_dialog.js","sourceRoot":"","sources":["../../src/mjr/ui_model_downloader_dialog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,GACX,MAAM,uBAAuB,CAAC;AAE/B,SAAS,gBAAgB,CAAC,MAAM;IAC9B,MAAM,KAAK,GAAG,EAAE,CAAC;IACjB,IAAI,MAAM,EAAE,GAAG;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,MAAM,EAAE,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC7D,IAAI,MAAM,EAAE,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,IAAI,MAAM,EAAE,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,uBAAuB,CAAC,OAAO,EAAE,QAAQ;IAChD,YAAY,EAAE,CAAC;IAEf,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,sBAAsB,CAAC;IAClD,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAC/B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;IACpC,OAAO,CAAC,KAAK,CAAC,cAAc,GAAG,QAAQ,CAAC;IACxC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAE/B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC5C,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,kBAAkB,CAAC;IACvC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;IAClD,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;IACxD,KAAK,CAAC,KAAK,CAAC,YAAY,GAAG,MAAM,CAAC;IAClC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;IACrC,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,6BAA6B,CAAC;IACtD,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAE3B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC5C,KAAK,CAAC,WAAW,GAAG,mBAAmB,OAAO,CAAC,MAAM,GAAG,CAAC;IACzD,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;IAC/B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,OAAO,CAAC;IACpC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAEzB,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAClD,WAAW,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IACnC,WAAW,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;IAC3C,WAAW,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC;IAE/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACjD,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAClC,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC;QAC5D,UAAU,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACtC,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,uBAAuB,CAAC;QACtD,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;QACpC,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC;QAEzC,UAAU,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;YAC7C,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YAC9D,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,yBAAyB,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;YAC7C,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC;YAC5D,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,uBAAuB,CAAC;QACxD,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACnC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAEhC,oBAAoB;QACpB,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;QACtC,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC;QAC5C,UAAU,CAAC,WAAW,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;QACjD,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC;QAC9B,UAAU,CAAC,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC;QAClC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QACrC,UAAU,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACtC,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;QAEpC,sBAAsB;QACtB,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;YAChB,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;YACzD,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YAC9D,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACrC,CAAC;aAAM,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;YACvB,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;YACzD,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YAC9D,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACrC,CAAC;aAAM,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;YACvB,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;YACzD,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YAC9D,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,KAAK,CAAC,UAAU,GAAG,yBAAyB,CAAC;YACxD,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAC7D,UAAU,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACrC,CAAC;QAED,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACrD,aAAa,CAAC,WAAW,GAAG,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,SAAS,CAAC;QACxE,aAAa,CAAC,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC;QACrC,aAAa,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QACxC,aAAa,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACzC,aAAa,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;QAEvC,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAClC,aAAa,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;YAC1D,aAAa,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAChE,aAAa,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACxC,CAAC;aAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC7C,aAAa,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;YAC1D,aAAa,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAChE,aAAa,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACxC,CAAC;aAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACxC,aAAa,CAAC,KAAK,CAAC,UAAU,GAAG,yBAAyB,CAAC;YAC3D,aAAa,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YACjE,aAAa,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QACxC,CAAC;aAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,YAAY,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YACzE,aAAa,CAAC,KAAK,CAAC,UAAU,GAAG,yBAAyB,CAAC;YAC3D,aAAa,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YACjE,aAAa,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;YACtC,aAAa,CAAC,WAAW,GAAG,KAAK,CAAC;QACpC,CAAC;QAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAChD,QAAQ,CAAC,WAAW,GAAG,MAAM,CAAC,IAAI,IAAI,SAAS,CAAC;QAChD,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC;QAE1B,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAChC,OAAO,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;QACnC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAE9B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC;QAEhC,MAAM,SAAS,GAAG,EAAE,CAAC;QACrB,IAAI,MAAM,CAAC,IAAI;YAAE,SAAS,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACxD,IAAI,MAAM,CAAC,OAAO;YAAE,SAAS,CAAC,IAAI,CAAC,QAAQ,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,OAAO;YAAE,SAAS,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,QAAQ;YAAE,SAAS,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEhE,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE5C,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAChC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAEhC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACxC,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QAEH,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAE/B,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACnD,SAAS,CAAC,WAAW,GAAG,QAAQ,CAAC;IACjC,SAAS,CAAC,SAAS,GAAG,YAAY,CAAC;IACnC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC;IACrC,SAAS,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;IACrC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC;IAC3D,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;IACtD,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;IAC/B,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;IACnC,SAAS,CAAC,KAAK,CAAC,SAAS,GAAG,UAAU,CAAC;IAEvC,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;QACvC,OAAO,CAAC,MAAM,EAAE,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAE7B,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;QACtC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE;IAClF,YAAY,EAAE,CAAC;IACf,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,WAAW,YAAY,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,sBAAsB,CAAC;QAClD,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,cAAc,GAAG,QAAQ,CAAC;QACxC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAE/B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,kBAAkB,CAAC;QACvC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC;QAC/B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAC9B,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;QAClD,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;QACxD,KAAK,CAAC,KAAK,CAAC,YAAY,GAAG,MAAM,CAAC;QAClC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,WAAW,CAAC;QAClC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC7B,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;QACrC,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC;QACzB,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;QAC3B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAC9B,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,6BAA6B,CAAC;QACtD,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAE3B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,KAAK,CAAC,WAAW,GAAG,yBAAyB,CAAC;QAC9C,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;QAC/B,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAC9B,KAAK,CAAC,KAAK,CAAC,aAAa,GAAG,OAAO,CAAC;QACpC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC/C,QAAQ,CAAC,WAAW,GAAG,oBAAoB,KAAK,CAAC,MAAM,EAAE,CAAC;QAC1D,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;QAC/B,QAAQ,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QACjC,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAE5B,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;QACvB,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAExB,MAAM,IAAI,GAAG,EAAE,CAAC;QAChB,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,KAAK,EAAE,MAAM,IAAI,IAAI,CAAC;YACrC,MAAM,WAAW,GAAG,KAAK,EAAE,SAAS,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC;YAC1D,MAAM,YAAY,GAAG,MAAM,EAAE,MAAM,KAAK,eAAe,CAAC;YAExD,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1C,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;YACnC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YACtB,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;YAC1B,GAAG,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YAC/B,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YACtD,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,uBAAuB,CAAC;YAC/C,MAAM,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;YACvC,MAAM,iBAAiB,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC;YAE/C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAC9B,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;YACnC,MAAM,CAAC,KAAK,CAAC,cAAc,GAAG,eAAe,CAAC;YAC9C,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAEzB,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YACjC,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;YACtC,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAC5B,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,SAAS,CAAC,WAAW,GAAG,KAAK,EAAE,aAAa,IAAI,KAAK,EAAE,GAAG,IAAI,eAAe,CAAC;YAC9E,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC;YACnC,SAAS,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAEjC,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;gBAC7C,KAAK,CAAC,WAAW,GAAG,oBAAoB,CAAC;gBACzC,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;gBAC9B,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;gBAChC,KAAK,CAAC,KAAK,CAAC,YAAY,GAAG,OAAO,CAAC;gBACnC,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;gBACpD,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,oCAAoC,CAAC;gBAC1D,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;gBAC9B,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC/B,CAAC;YAED,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACrD,YAAY,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YACpC,YAAY,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;YACzC,YAAY,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAC/B,YAAY,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;YACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACnD,UAAU,CAAC,IAAI,GAAG,UAAU,CAAC;YAC7B,UAAU,CAAC,OAAO,GAAG,WAAW,CAAC;YACjC,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YACpD,YAAY,CAAC,WAAW,GAAG,UAAU,CAAC;YACtC,YAAY,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YACpC,YAAY,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;YACrC,YAAY,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;YAEvC,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAC9B,MAAM,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;YAEjC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACxB,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;YAC/B,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;YACtD,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC;YACjC,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAE3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC;YAC7B,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;YAC/B,QAAQ,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;YACjC,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjD,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC;YACvB,QAAQ,CAAC,WAAW,GAAG,aAAa,CAAC;YACrC,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC9B,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YACnC,QAAQ,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YACpC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAC3D,QAAQ,CAAC,KAAK,CAAC,UAAU,GAAG,kBAAkB,CAAC;YAC/C,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAE9B,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,SAAS,CAAC,WAAW,GAAG,MAAM,CAAC;YAC/B,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;YAChC,SAAS,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;YAClC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YACnD,SAAS,CAAC,SAAS,GAAG,eAAe,CAAC;YACtC,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC/B,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YACpC,SAAS,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YACrC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAC5D,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;YAClC,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;gBAC3B,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;gBAC7C,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;gBACzB,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;gBAC/C,SAAS,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;YAC7B,CAAC;YAED,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAChD,SAAS,CAAC,WAAW,GAAG,qBAAqB,CAAC;YAC9C,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;YAChC,SAAS,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;YAClC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAClD,SAAS,CAAC,IAAI,GAAG,MAAM,CAAC;YACxB,SAAS,CAAC,WAAW,GAAG,oBAAoB,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1D,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC/B,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YACpC,SAAS,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YACrC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAC5D,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,kBAAkB,CAAC;YAChD,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAE/B,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,QAAQ,CAAC,WAAW,GAAG,mBAAmB,CAAC;YAC3C,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;YAC/B,QAAQ,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;YACjC,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YACjD,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC;YACvB,QAAQ,CAAC,WAAW,GAAG,cAAc,CAAC;YACtC,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC9B,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YACnC,QAAQ,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YACpC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;YAC3D,QAAQ,CAAC,KAAK,CAAC,UAAU,GAAG,kBAAkB,CAAC;YAC/C,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAE9B,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAC5B,IAAI,CAAC,KAAK,CAAC,mBAAmB,GAAG,SAAS,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAEvB,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC9C,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAC/B,OAAO,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;YACvC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAC1B,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC9B,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC9B,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAC/B,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAE/B,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC/C,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;YAChC,QAAQ,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC;YACxC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;YAC3B,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAChC,QAAQ,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YAChC,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC/B,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAE/B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAC1B,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC3B,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YAEtB,6BAA6B;YAC7B,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACpD,aAAa,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC;YACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YACnD,SAAS,CAAC,WAAW,GAAG,kBAAkB,CAAC;YAC3C,SAAS,CAAC,SAAS,GAAG,YAAY,CAAC;YACnC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;YACpC,SAAS,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;YACrC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,mCAAmC,CAAC;YAC7D,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;YACxD,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC/B,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;YACnC,SAAS,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;YAClC,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAE/B,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBAC7C,+CAA+C;gBAC/C,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,KAAK,EAAE,aAAa,IAAI,EAAE,CAAC;qBAC3D,OAAO,CAAC,mCAAmC,EAAE,EAAE,CAAC,CAAC;gBAEpD,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,KAAK,CAAC,2CAA2C,CAAC,CAAC;oBACnD,OAAO;gBACT,CAAC;gBAED,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC;gBAC1B,SAAS,CAAC,WAAW,GAAG,gBAAgB,CAAC;gBAEzC,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,2BAA2B,EAAE;wBACxD,MAAM,EAAE,MAAM;wBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;wBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;qBACvD,CAAC,CAAC;oBAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;oBACnC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;wBACb,KAAK,CAAC,eAAe,CAAC,CAAC;wBACvB,OAAO;oBACT,CAAC;oBAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;oBAC7B,0DAA0D;oBAC1D,MAAM,UAAU,GAAG,OAAO,CAAC,cAAc,IAAI;wBAC3C,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,IAAI,EAAE,CAAC;wBACrC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,IAAI,EAAE,CAAC;wBACzC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,IAAI,EAAE,CAAC;qBACrC,CAAC;oBAEF,oDAAoD;oBACpD,MAAM,SAAS,GAAG,OAAO,CAAC,iBAAiB,CAAC;oBAE5C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBAC5B,MAAM,OAAO,GAAG,yBAAyB,WAAW,iHAAiH,CAAC;wBAEtK,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;4BACrB,gCAAgC;4BAChC,IAAI,SAAS,EAAE,CAAC;gCACd,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;4BACnC,CAAC;wBACH,CAAC;wBACD,OAAO;oBACT,CAAC;oBAED,0DAA0D;oBAC1D,uBAAuB,CAAC,UAAU,EAAE,CAAC,cAAc,EAAE,EAAE;wBACrD,IAAI,cAAc,EAAE,CAAC;4BACnB,QAAQ,CAAC,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC;4BACpC,IAAI,cAAc,CAAC,QAAQ;gCAAE,SAAS,CAAC,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC;4BACvE,IAAI,cAAc,CAAC,MAAM;gCAAE,QAAQ,CAAC,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC;4BAClE,IAAI,cAAc,CAAC,IAAI;gCAAE,YAAY,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;4BAC3D,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;4BAC1B,qBAAqB,EAAE,CAAC;wBAC1B,CAAC;oBACH,CAAC,CAAC,CAAC;gBAEL,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;oBACtC,KAAK,CAAC,gBAAgB,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC1C,CAAC;wBAAS,CAAC;oBACT,SAAS,CAAC,QAAQ,GAAG,KAAK,CAAC;oBAC3B,SAAS,CAAC,WAAW,GAAG,kBAAkB,CAAC;gBAC7C,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,aAAa,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YACrC,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;YAE/B,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;gBACjD,UAAU,CAAC,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;gBAClD,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;gBAClC,UAAU,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;gBACnC,UAAU,CAAC,KAAK,CAAC,SAAS,GAAG,YAAY,CAAC;gBAC1C,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;YAC9B,CAAC;YAED,IAAI,MAAM,EAAE,GAAG,EAAE,CAAC;gBAChB,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC;YAC9B,CAAC;YACD,IAAI,MAAM,EAAE,QAAQ,EAAE,CAAC;gBACrB,SAAS,CAAC,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC;YACpC,CAAC;YACD,IAAI,MAAM,EAAE,MAAM,EAAE,CAAC;gBACnB,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;YACjC,CAAC;YACD,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,EAAE;gBAC7B,IAAI,CAAC,KAAK;oBAAE,OAAO,KAAK,CAAC;gBACzB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;gBAChF,IAAI,MAAM,EAAE,CAAC;oBACX,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC;oBACxB,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,OAAO,KAAK,CAAC;YACf,CAAC,CAAC;YAEF,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;gBAChC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC5B,CAAC;YAED,IAAI,MAAM,GAAG,WAAW,CAAC;YACzB,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;gBAC1B,MAAM,GAAG,KAAK,CAAC;gBACf,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC;gBAC3B,SAAS,CAAC,QAAQ,GAAG,MAAM,CAAC;gBAC5B,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC;gBAC3B,SAAS,CAAC,QAAQ,GAAG,MAAM,CAAC;YAC9B,CAAC,CAAC;YACF,SAAS,CAAC,WAAW,CAAC,CAAC;YAEvB,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;gBACjD,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC;gBAC7B,OAAO,CAAC,SAAS,GAAG,YAAY,CAAC;gBACjC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;gBAClC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;gBACnC,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC;gBACzD,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;gBACpD,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;gBAC7B,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;gBACjC,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;oBACrC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;oBACnB,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;gBACjD,CAAC,CAAC,CAAC;gBACH,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;YAED,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;YACvF,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;gBACnC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ;oBAAE,OAAO,KAAK,CAAC;gBAClD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAC/B,IAAI,CAAC,GAAG;oBAAE,OAAO,KAAK,CAAC;gBACvB,OAAO,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC1C,CAAC,CAAC;YAEF,MAAM,qBAAqB,GAAG,GAAG,EAAE;gBACjC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,IAAI,sBAAsB,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACtG,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACd,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC;oBACjC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,iBAAiB,CAAC;oBACzC,OAAO;gBACT,CAAC;gBACD,IAAI,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;oBAC9B,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,kCAAkC,CAAC;oBACtD,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,yBAAyB,CAAC;gBACnD,CAAC;qBAAM,CAAC;oBACN,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,aAAa,CAAC;oBACjC,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,iBAAiB,CAAC;gBAC3C,CAAC;YACH,CAAC,CAAC;YAEF,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;YAC1D,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;YAC3D,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC;YAC5D,qBAAqB,EAAE,CAAC;YAExB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;YACtB,IAAI,CAAC,IAAI,CAAC;gBACR,KAAK;gBACL,UAAU;gBACV,QAAQ,EAAE,QAAQ;gBAClB,UAAU,EAAE,SAAS;gBACrB,aAAa,EAAE,SAAS;gBACxB,QAAQ,EAAE,QAAQ;aACnB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACrD,YAAY,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACpC,YAAY,CAAC,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC;QACzC,YAAY,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;QAC/B,YAAY,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;QACtC,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACnD,UAAU,CAAC,IAAI,GAAG,UAAU,CAAC;QAC7B,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACpD,YAAY,CAAC,WAAW,GAAG,wBAAwB,CAAC;QACpD,YAAY,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QACpC,YAAY,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QACrC,YAAY,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QACvC,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;QAEhC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAChD,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;QAClC,SAAS,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC;QAClC,SAAS,CAAC,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC;QACnC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAE7B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC;QAC1B,OAAO,CAAC,KAAK,CAAC,cAAc,GAAG,UAAU,CAAC;QAE1C,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACnD,SAAS,CAAC,WAAW,GAAG,QAAQ,CAAC;QACjC,SAAS,CAAC,SAAS,GAAG,YAAY,CAAC;QACnC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC;QACrC,SAAS,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACrC,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC;QAC3D,SAAS,CAAC,KAAK,CAAC,UAAU,GAAG,wBAAwB,CAAC;QACtD,SAAS,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;QAC/B,SAAS,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;QAEnC,MAAM,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACrD,WAAW,CAAC,WAAW,GAAG,mBAAmB,CAAC;QAC9C,WAAW,CAAC,SAAS,GAAG,YAAY,CAAC;QACrC,WAAW,CAAC,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC;QACvC,WAAW,CAAC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC;QACvC,WAAW,CAAC,KAAK,CAAC,MAAM,GAAG,oCAAoC,CAAC;QAChE,WAAW,CAAC,KAAK,CAAC,UAAU,GAAG,0BAA0B,CAAC;QAC1D,WAAW,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;QACjC,WAAW,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC;QAErC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAC/B,OAAO,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;QACjC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAE3B,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,EAAE;YACtB,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACnD,IAAI,OAAO,CAAC,UAAU;gBAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACzC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC;QAEF,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;YAC1B,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,QAAQ,EAAE,CAAC;gBACvD,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,IAAI,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC;QAEF,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACzC,SAAS,CAAC,WAAW,GAAG,EAAE,CAAC;YAC3B,MAAM,aAAa,GAAG,EAAE,CAAC;YACzB,MAAM,SAAS,GAAG,EAAE,CAAC;YACrB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO;oBAAE,SAAS;gBACtC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACrD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACxD,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBAClE,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;gBAEtE,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC7B,SAAS,CAAC,WAAW,GAAG,wCAAwC,CAAC;oBACjE,OAAO;gBACT,CAAC;gBAED,MAAM,QAAQ,GAAG,WAAW,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;gBAC5D,IAAI,CAAC,QAAQ,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAChD,SAAS,CAAC,WAAW,GAAG,0CAA0C,CAAC;oBACnE,OAAO;gBACT,CAAC;gBAED,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,SAAS,CAAC,WAAW,GAAG,6BAA6B,CAAC;oBACtD,OAAO;gBACT,CAAC;gBAED,IAAI,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC7C,SAAS,CAAC,WAAW,GAAG,mCAAmC,CAAC;oBAC5D,OAAO;gBACT,CAAC;gBAED,MAAM,IAAI,GAAG;oBACX,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,IAAI;oBACJ,GAAG;oBACH,QAAQ;oBACR,MAAM,EAAE,MAAM,IAAI,IAAI;iBACvB,CAAC;gBACF,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACzB,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;oBACvB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YAED,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC;gBAC1B,SAAS,CAAC,WAAW,GAAG,uCAAuC,CAAC;gBAChE,OAAO;YACT,CAAC;YAED,KAAK,CAAC;gBACJ,KAAK,EAAE,aAAa;gBACpB,SAAS;gBACT,QAAQ,EAAE,UAAU,CAAC,OAAO;aAC7B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/js/mjr/ui_model_fixer_dialog.d.ts b/js/mjr/ui_model_fixer_dialog.d.ts new file mode 100644 index 0000000..f361a74 --- /dev/null +++ b/js/mjr/ui_model_fixer_dialog.d.ts @@ -0,0 +1 @@ +export declare function showModelFixerDialog({ missing, results }?: {}): Promise; diff --git a/js/mjr/ui_model_fixer_dialog.js b/js/mjr/ui_model_fixer_dialog.js index 0453b91..c1f9868 100644 --- a/js/mjr/ui_model_fixer_dialog.js +++ b/js/mjr/ui_model_fixer_dialog.js @@ -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 \ No newline at end of file diff --git a/js/mjr/ui_model_fixer_dialog.js.map b/js/mjr/ui_model_fixer_dialog.js.map new file mode 100644 index 0000000..b8b7e39 --- /dev/null +++ b/js/mjr/ui_model_fixer_dialog.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ui_model_fixer_dialog.js","sourceRoot":"","sources":["../../src/mjr/ui_model_fixer_dialog.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/utils.d.ts b/js/mjr/utils.d.ts new file mode 100644 index 0000000..44e8245 --- /dev/null +++ b/js/mjr/utils.d.ts @@ -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; diff --git a/js/mjr/utils.js b/js/mjr/utils.js index 0998595..b57a533 100644 --- a/js/mjr/utils.js +++ b/js/mjr/utils.js @@ -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 \ No newline at end of file diff --git a/js/mjr/utils.js.map b/js/mjr/utils.js.map new file mode 100644 index 0000000..e1bf762 --- /dev/null +++ b/js/mjr/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/mjr/utils.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/mjr/workflow_note_recipes.d.ts b/js/mjr/workflow_note_recipes.d.ts new file mode 100644 index 0000000..a928201 --- /dev/null +++ b/js/mjr/workflow_note_recipes.d.ts @@ -0,0 +1,3 @@ +export declare function extractNoteTexts(workflowJson: any): unknown[]; +export declare function parseRecipesFromNoteText(text: any): Map; +export declare function collectRecipesFromWorkflowNotes(workflowJson: any): Map; diff --git a/js/mjr/workflow_note_recipes.js b/js/mjr/workflow_note_recipes.js index 2b654e6..b82e8cd 100644 --- a/js/mjr/workflow_note_recipes.js +++ b/js/mjr/workflow_note_recipes.js @@ -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 \ No newline at end of file diff --git a/js/mjr/workflow_note_recipes.js.map b/js/mjr/workflow_note_recipes.js.map new file mode 100644 index 0000000..981c244 --- /dev/null +++ b/js/mjr/workflow_note_recipes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"workflow_note_recipes.js","sourceRoot":"","sources":["../../src/mjr/workflow_note_recipes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/state_manager.d.ts b/js/state_manager.d.ts new file mode 100644 index 0000000..7567b83 --- /dev/null +++ b/js/state_manager.d.ts @@ -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; diff --git a/js/state_manager.js b/js/state_manager.js index 94fe3f1..2f0b210 100644 --- a/js/state_manager.js +++ b/js/state_manager.js @@ -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 \ No newline at end of file diff --git a/js/state_manager.js.map b/js/state_manager.js.map new file mode 100644 index 0000000..044aac1 --- /dev/null +++ b/js/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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"} \ No newline at end of file diff --git a/js/ui_components.d.ts b/js/ui_components.d.ts new file mode 100644 index 0000000..62907f0 --- /dev/null +++ b/js/ui_components.d.ts @@ -0,0 +1 @@ +export declare function buildPanel(el: any, state: any, actions: any): void; diff --git a/js/ui_components.js b/js/ui_components.js index 0ef1057..1ee305a 100644 --- a/js/ui_components.js +++ b/js/ui_components.js @@ -1,34 +1,10 @@ -import { app } from "../../scripts/app.js"; +import { app } from "../../scripts/app.js"; import { joinRel, makeKindToken, mediaDir, titlePathJS, token3Tag, yymmddJS } from "./mjr/utils.js"; import { ensureStyles, toast } from "./mjr/toast.js"; -import { - buildFingerprintCache, - downloadModels, - fetchJSON, - getDownloadStatus, - getFingerprintCacheStatus, - getModels, - listExistingNames, - previewTemplate, - resolveModelRecipes, - saveModelRecipes, - scanModelCandidates, - searchRegistry, -} from "./mjr/api.js"; +import { buildFingerprintCache, downloadModels, fetchJSON, getDownloadStatus, getFingerprintCacheStatus, getModels, listExistingNames, previewTemplate, resolveModelRecipes, saveModelRecipes, scanModelCandidates, searchRegistry, } from "./mjr/api.js"; import { psConfirm, psPrompt } from "./mjr/dialog.js"; -import { - alreadyProjectPathed, - detectNodeMedia, - isSaveLikeNode, - patchSaveNodes, - patchSingleNode, - stampGraphProjectSignature, -} from "./mjr/patch.js"; -import { - applyFixesToGraph, - scanMissingModelsFromGraph, - scanMissingModelsWithStats, -} from "./mjr/model_fixer.js"; +import { isSaveLikeNode, patchSaveNodes, stampGraphProjectSignature, } from "./mjr/patch.js"; +import { applyFixesToGraph, scanMissingModelsWithStats, } from "./mjr/model_fixer.js"; import { collectNoteRecipes, getKindOptions, normalizeKey, typeHintToKind } from "./mjr/model_downloader.js"; import { showModelFixerDialog } from "./mjr/ui_model_fixer_dialog.js"; import { showModelDownloaderDialog } from "./mjr/ui_model_downloader_dialog.js"; @@ -37,2355 +13,2168 @@ import { saveState } from "./state_manager.js"; import { getSerializedWorkflow } from "./mjr/graph.js"; import { extractNoteTexts } from "./mjr/workflow_note_recipes.js"; import { debug } from "./mjr/log.js"; - const TEMPLATE_TOKENS = ["{BASE}", "{MEDIA}", "{DATE}", "{MODEL}", "{NAME}", "{KIND}"]; - const PROJECT_REFRESH_THROTTLE_MS = 5000; const PROJECT_REFRESH_ERROR_BACKOFF_MS = 10000; const SETTING_AUTO_FIX_AFTER_DOWNLOAD = "mjr_project.auto_fix_after_download"; const SETTING_AUTO_SCAN_MISSING_ON_OPEN = "mjr_project.auto_scan_missing_on_open"; const SETTING_AUTO_REMEMBER_NOTE_SOURCES = "mjr_project.auto_remember_note_sources"; const SETTING_NO_PROJECT_STATUS_BLACK = "mjr_project.status_black_no_project"; - 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?.ui?.settings?.settingsValues?.[id]; - return toBool(raw, fallback); -}; - -function parseHexColor(hex) { - const m = String(hex || "").trim().match(/^#([0-9a-fA-F]{6})$/); - if (!m) return null; - const v = m[1]; - return [ - parseInt(v.slice(0, 2), 16), - parseInt(v.slice(2, 4), 16), - parseInt(v.slice(4, 6), 16), - ]; -} - -function colorWithAlpha(hex, alpha) { - const rgb = parseHexColor(hex); - if (!rgb) return hex; - const a = Math.max(0, Math.min(1, Number(alpha))); - return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${a})`; -} - -function makeStatus(state) { - const det = state.detected; - const sameProject = - det && - det.mode !== "none" && - ((det.project_id && det.project_id === state.projectId) || - (det.project_folder && det.project_folder === state.projectFolder)); - const mismatch = - det && - det.mode !== "none" && - !sameProject && - (det.project_id || det.project_folder); - - if (state.lastError) { - return { color: "#b33a3a", text: `Error: ${state.lastError}`, level: "red" }; - } - if (state.graphIsEmpty) { - return { color: "#b33a3a", text: "Empty graph, set or create a project", level: "red" }; - } - if (!state.projectId) { - if (getSettingBool(SETTING_NO_PROJECT_STATUS_BLACK, false)) { - return { color: "#000000", text: "No project (test)", level: "black" }; + 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 { color: "#666666", text: "No active project", level: "gray" }; - } - if (state.projectExists === false) { + return fallback; +}; +const getSettingBool = (id, fallback) => { + const raw = app?.ui?.settings?.settingsValues?.[id]; + return toBool(raw, fallback); +}; +function parseHexColor(hex) { + const m = String(hex || "").trim().match(/^#([0-9a-fA-F]{6})$/); + if (!m) + return null; + const v = m[1]; + return [ + parseInt(v.slice(0, 2), 16), + parseInt(v.slice(2, 4), 16), + parseInt(v.slice(4, 6), 16), + ]; +} +function colorWithAlpha(hex, alpha) { + const rgb = parseHexColor(hex); + if (!rgb) + return hex; + const a = Math.max(0, Math.min(1, Number(alpha))); + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${a})`; +} +function makeStatus(state) { + const det = state.detected; + const sameProject = det && + det.mode !== "none" && + ((det.project_id && det.project_id === state.projectId) || + (det.project_folder && det.project_folder === state.projectFolder)); + const mismatch = det && + det.mode !== "none" && + !sameProject && + (det.project_id || det.project_folder); + if (state.lastError) { + return { color: "#b33a3a", text: `Error: ${state.lastError}`, level: "red" }; + } + if (state.graphIsEmpty) { + return { color: "#b33a3a", text: "Empty graph, set or create a project", level: "red" }; + } + if (!state.projectId) { + if (getSettingBool(SETTING_NO_PROJECT_STATUS_BLACK, false)) { + return { color: "#000000", text: "No project (test)", level: "black" }; + } + return { color: "#666666", text: "No active project", level: "gray" }; + } + if (state.projectExists === false) { + return { + color: "#b33a3a", + text: `Missing project folder: ${state.projectFolder || state.projectId}`, + level: "red", + }; + } + if (state.workflowChecked && !state.workflowHasSignature) { + return { + color: "#c77c2a", + text: "Active project, workflow unassigned", + level: "orange", + }; + } + if (mismatch) { + return { + color: "#c77c2a", + text: `Project mismatch: ${det.project_folder || det.project_id}`, + level: "orange", + }; + } return { - color: "#b33a3a", - text: `Missing project folder: ${state.projectFolder || state.projectId}`, - level: "red", + color: "#2f8f4e", + text: `Active: ${state.projectFolder} (${state.projectId})`, + level: "green", }; - } - if (state.workflowChecked && !state.workflowHasSignature) { - return { - color: "#c77c2a", - text: "Active project, workflow unassigned", - level: "orange", - }; - } - if (mismatch) { - return { - color: "#c77c2a", - text: `Project mismatch: ${det.project_folder || det.project_id}`, - level: "orange", - }; - } - return { - color: "#2f8f4e", - text: `Active: ${state.projectFolder} (${state.projectId})`, - level: "green", - }; } - const MENU_BADGE_ID = "mjr-project-status-badge"; const MENU_ACTION_SELECTORS = [ - ".comfy-menu-actions", - ".comfy-menu-no-drag", - ".comfy-topbar-actions", - ".comfy-menu", - ".workflow-tabs-container", - ".workflow-tabs-container .flex", - ".menu-actions" + ".comfy-menu-actions", + ".comfy-menu-no-drag", + ".comfy-topbar-actions", + ".comfy-menu", + ".workflow-tabs-container", + ".workflow-tabs-container .flex", + ".menu-actions" ]; - let menuBadgeInterval = null; let menuBadgePendingState = null; let menuBadgeLogTimestamp = 0; - function findMenuActionsContainer() { - for (const selector of MENU_ACTION_SELECTORS) { - const candidate = document.querySelector(selector); - if (candidate) { - return candidate; - } - } - return null; -} - -function ensureMenuStatusBadge(actions, text, color) { - if (!actions) return null; - let badge = document.getElementById(MENU_BADGE_ID); - if (!badge) { - badge = document.createElement("div"); - badge.id = MENU_BADGE_ID; - badge.style.display = "inline-flex"; - badge.style.alignItems = "center"; - badge.style.justifyContent = "center"; - badge.style.padding = "4px 10px"; - badge.style.borderRadius = "999px"; - badge.style.fontSize = "11px"; - badge.style.fontWeight = "600"; - badge.style.border = "1px solid transparent"; - badge.style.marginLeft = "8px"; - badge.style.gap = "4px"; - badge.style.transition = "background 0.3s ease, border 0.3s ease"; - badge.style.cursor = "default"; - actions.appendChild(badge); - } - if (text) badge.textContent = text; - if (color) { - badge.style.background = color; - badge.style.borderColor = color; - } - return badge; -} - -function updateMenuStatusBadge(state) { - menuBadgePendingState = state; - const actions = findMenuActionsContainer(); - if (!actions) { - if (!menuBadgeInterval) { - menuBadgeInterval = setInterval(() => { - if (menuBadgePendingState) { - updateMenuStatusBadge(menuBadgePendingState); + for (const selector of MENU_ACTION_SELECTORS) { + const candidate = document.querySelector(selector); + if (candidate) { + return candidate; } - }, 500); } - const now = Date.now(); - if (now - menuBadgeLogTimestamp > 5000) { - console.info( - `[mjr] Waiting for topbar badge target (${MENU_ACTION_SELECTORS.join( - ", " - )}) project=${state?.projectId || "none"} folder=${state?.projectFolder || "none"}` - ); - menuBadgeLogTimestamp = now; + return null; +} +function ensureMenuStatusBadge(actions, text, color) { + if (!actions) + return null; + let badge = document.getElementById(MENU_BADGE_ID); + if (!badge) { + badge = document.createElement("div"); + badge.id = MENU_BADGE_ID; + badge.style.display = "inline-flex"; + badge.style.alignItems = "center"; + badge.style.justifyContent = "center"; + badge.style.padding = "4px 10px"; + badge.style.borderRadius = "999px"; + badge.style.fontSize = "11px"; + badge.style.fontWeight = "600"; + badge.style.border = "1px solid transparent"; + badge.style.marginLeft = "8px"; + badge.style.gap = "4px"; + badge.style.transition = "background 0.3s ease, border 0.3s ease"; + badge.style.cursor = "default"; + actions.appendChild(badge); } - console.debug("[mjr] comfy menu actions not found yet, retrying badge soon", state); - return; - } - - if (menuBadgeInterval) { - clearInterval(menuBadgeInterval); - menuBadgeInterval = null; - } - - const badgeText = state.projectId - ? state.projectExists === false - ? "Projet introuvable" - : `Projet: ${state.projectFolder || state.projectId}` - : "Pas de projet actif"; - const badgeColor = state.projectId - ? state.projectExists === false - ? "rgba(179, 58, 58, 0.9)" - : "rgba(47, 143, 78, 0.95)" - : "rgba(102, 102, 102, 0.8)"; - ensureMenuStatusBadge(actions, badgeText, badgeColor); - menuBadgePendingState = null; + if (text) + badge.textContent = text; + if (color) { + badge.style.background = color; + badge.style.borderColor = color; + } + return badge; +} +function updateMenuStatusBadge(state) { + menuBadgePendingState = state; + const actions = findMenuActionsContainer(); + if (!actions) { + if (!menuBadgeInterval) { + menuBadgeInterval = setInterval(() => { + if (menuBadgePendingState) { + updateMenuStatusBadge(menuBadgePendingState); + } + }, 500); + } + const now = Date.now(); + if (now - menuBadgeLogTimestamp > 5000) { + console.info(`[mjr] Waiting for topbar badge target (${MENU_ACTION_SELECTORS.join(", ")}) project=${state?.projectId || "none"} folder=${state?.projectFolder || "none"}`); + menuBadgeLogTimestamp = now; + } + console.debug("[mjr] comfy menu actions not found yet, retrying badge soon", state); + return; + } + if (menuBadgeInterval) { + clearInterval(menuBadgeInterval); + menuBadgeInterval = null; + } + const badgeText = state.projectId + ? state.projectExists === false + ? "Projet introuvable" + : `Projet: ${state.projectFolder || state.projectId}` + : "Pas de projet actif"; + const badgeColor = state.projectId + ? state.projectExists === false + ? "rgba(179, 58, 58, 0.9)" + : "rgba(47, 143, 78, 0.95)" + : "rgba(102, 102, 102, 0.8)"; + ensureMenuStatusBadge(actions, badgeText, badgeColor); + menuBadgePendingState = null; } export function buildPanel(el, state, actions) { - const { createAndActivateProject, setActiveProjectById, saveWorkflowToProject } = actions || {}; - el.innerHTML = ""; - ensureStyles(); - - const UI_GAP = "8px"; - - const container = document.createElement("div"); - container.style.display = "flex"; - container.style.flexDirection = "column"; - container.style.gap = UI_GAP; - container.style.fontSize = "12px"; - container.style.color = "#ddd"; - container.style.fontFamily = "inherit"; - container.style.padding = "8px 14px 16px"; - - const section = (label) => { - const h = document.createElement("div"); - h.textContent = label; - h.style.fontWeight = "600"; - h.style.marginTop = UI_GAP; - h.style.marginBottom = UI_GAP; - h.style.paddingLeft = "2px"; - h.style.textTransform = "uppercase"; - h.style.letterSpacing = "0.6px"; - h.style.color = "#8fb9ff"; - return h; - }; - - const divider = (thick = false) => { - const d = document.createElement("div"); - d.style.height = thick ? "3px" : "1px"; - d.style.background = "rgba(255,255,255,0.08)"; - d.style.margin = "0"; - return d; - }; - - const detailsSection = (title) => { - const details = document.createElement("details"); - details.style.marginTop = "0"; - details.style.display = "flex"; - details.style.flexDirection = "column"; - details.style.gap = UI_GAP; + const { createAndActivateProject, setActiveProjectById, saveWorkflowToProject } = actions || {}; + el.innerHTML = ""; + ensureStyles(); + const UI_GAP = "8px"; + const container = document.createElement("div"); + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = UI_GAP; + container.style.fontSize = "12px"; + container.style.color = "#ddd"; + container.style.fontFamily = "inherit"; + container.style.padding = "8px 14px 16px"; + const section = (label) => { + const h = document.createElement("div"); + h.textContent = label; + h.style.fontWeight = "600"; + h.style.marginTop = UI_GAP; + h.style.marginBottom = UI_GAP; + h.style.paddingLeft = "2px"; + h.style.textTransform = "uppercase"; + h.style.letterSpacing = "0.6px"; + h.style.color = "#8fb9ff"; + return h; + }; + const divider = (thick = false) => { + const d = document.createElement("div"); + d.style.height = thick ? "3px" : "1px"; + d.style.background = "rgba(255,255,255,0.08)"; + d.style.margin = "0"; + return d; + }; + const detailsSection = (title) => { + const details = document.createElement("details"); + details.style.marginTop = "0"; + details.style.display = "flex"; + details.style.flexDirection = "column"; + details.style.gap = UI_GAP; + const summary = document.createElement("summary"); + summary.textContent = title; + summary.style.cursor = "pointer"; + summary.style.opacity = "0.9"; + summary.style.textTransform = "uppercase"; + summary.style.letterSpacing = "0.6px"; + summary.style.color = "#8fb9ff"; + summary.style.fontWeight = "600"; + summary.style.paddingLeft = "2px"; + details.appendChild(summary); + details._summary = summary; + return details; + }; + const makeLabel = (labelText) => { + const label = document.createElement("label"); + label.textContent = labelText; + label.style.opacity = "0.85"; + label.style.marginLeft = "2px"; + label.style.fontSize = "11px"; + label.style.lineHeight = "1.2"; + return label; + }; + const styleField = (inputEl) => { + inputEl.style.width = "100%"; + inputEl.style.boxSizing = "border-box"; + inputEl.style.padding = "6px 8px"; + inputEl.style.borderRadius = "8px"; + inputEl.style.border = "1px solid rgba(255,255,255,0.12)"; + inputEl.style.background = "rgba(0,0,0,0.25)"; + inputEl.style.color = "#eee"; + inputEl.style.outline = "none"; + }; + const row = (labelText, inputEl) => { + const wrap = document.createElement("div"); + wrap.style.display = "flex"; + wrap.style.flexDirection = "column"; + wrap.style.gap = UI_GAP; + wrap.appendChild(makeLabel(labelText)); + styleField(inputEl); + wrap.appendChild(inputEl); + return wrap; + }; + const groupRow = (labelText, contentEl) => { + const wrap = document.createElement("div"); + wrap.style.display = "flex"; + wrap.style.flexDirection = "column"; + wrap.style.gap = UI_GAP; + wrap.appendChild(makeLabel(labelText)); + wrap.appendChild(contentEl); + return wrap; + }; + const btn = (text) => { + const b = document.createElement("button"); + b.textContent = text; + b.className = "mjr-ps-btn"; + b.style.padding = "7px 8px"; + b.style.borderRadius = "8px"; + b.style.border = "1px solid rgba(46, 136, 255, 0.65)"; + b.style.background = "rgba(46, 136, 255, 0.25)"; + b.style.color = "#eee"; + b.style.cursor = "pointer"; + b.style.margin = "0"; + b.style.lineHeight = "1.2"; + b.style.whiteSpace = "nowrap"; + return b; + }; + // Status bar + const statusBar = document.createElement("div"); + statusBar.style.display = "flex"; + statusBar.style.alignItems = "center"; + statusBar.style.gap = UI_GAP; + statusBar.style.padding = "6px 8px"; + statusBar.style.borderRadius = "8px"; + statusBar.style.position = "sticky"; + statusBar.style.top = "0"; + statusBar.style.zIndex = "2"; + statusBar.style.background = "rgba(10, 12, 16, 0.72)"; + statusBar.style.backdropFilter = "blur(6px)"; + statusBar.style.cursor = "pointer"; + statusBar.title = "Toggle workflow project panel"; + const statusDot = document.createElement("div"); + statusDot.style.width = "8px"; + statusDot.style.height = "8px"; + statusDot.style.borderRadius = "50%"; + const statusText = document.createElement("div"); + statusText.style.opacity = "0.95"; + const updateStatus = () => { + const s = makeStatus(state); + const level = s.level || "gray"; + statusBar.style.background = colorWithAlpha(s.color, 0.92); + statusBar.style.border = `1px solid ${colorWithAlpha(s.color, 0.35)}`; + statusDot.style.background = s.color; + statusText.textContent = s.text; + if (workflowSaveWrap) { + workflowSaveWrap.style.border = `1px solid ${colorWithAlpha(s.color, 0.4)}`; + workflowSaveWrap.style.background = colorWithAlpha(s.color, 0.12); + } + if (workflowWrap) { + const emphasize = level === "orange" && state.workflowChecked && !state.workflowHasSignature; + workflowWrap.style.border = emphasize + ? `1px solid ${colorWithAlpha("#c77c2a", 0.7)}` + : "1px solid rgba(255,255,255,0.12)"; + workflowWrap.style.background = emphasize + ? colorWithAlpha("#c77c2a", 0.1) + : "rgba(15, 17, 22, 0.75)"; + } + updateMenuStatusBadge(state); + }; + statusBar.appendChild(statusDot); + statusBar.appendChild(statusText); + statusBar.addEventListener("click", () => { + state.workflowPanelOpen = !state.workflowPanelOpen; + saveState(state); + updateWorkflowBlock(); + }); + const updateResolve = () => { + const det = state.detected; + let show = true; + if (!det || det.mode === "none") { + show = false; + } + const sameProject = det && + det.mode !== "none" && + ((det.project_id && det.project_id === state.projectId) || + (det.project_folder && det.project_folder === state.projectFolder)); + if (sameProject) { + show = false; + } + if (!show) { + resolveWrap.style.display = "none"; + return; + } + if (state.workflowPanelOpen) { + state.workflowPanelOpen = false; + saveState(state); + } + const mode = det.mode || "none"; + const wfFolder = det.project_folder || "none"; + resolveInfo.textContent = `Workflow project: ${wfFolder} (mode: ${mode})`; + resolveActive.textContent = `Active project: ${state.projectFolder || "none"}`; + resolveWrap.style.display = "flex"; + const canSwitch = mode !== "none" && (det.project_id || det.project_folder); + switchBtn.disabled = !canSwitch; + switchBtn.style.opacity = canSwitch ? "1" : "0.5"; + const canAssign = !!state.projectId; + assignBtn.disabled = !canAssign; + assignBtn.style.opacity = canAssign ? "1" : "0.5"; + }; + const resolveWrap = document.createElement("div"); + resolveWrap.style.display = "none"; + resolveWrap.style.flexDirection = "column"; + resolveWrap.style.gap = UI_GAP; + resolveWrap.style.padding = "8px 10px"; + resolveWrap.style.borderRadius = "8px"; + resolveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + resolveWrap.style.background = "rgba(15, 17, 22, 0.75)"; + resolveWrap.style.backdropFilter = "blur(6px)"; + const resolveTitle = document.createElement("div"); + resolveTitle.textContent = "Workflow project"; + resolveTitle.style.textTransform = "uppercase"; + resolveTitle.style.letterSpacing = "0.6px"; + resolveTitle.style.fontWeight = "600"; + resolveTitle.style.color = "#8fb9ff"; + resolveTitle.style.marginBottom = "0"; + const resolveInfo = document.createElement("div"); + resolveInfo.style.opacity = "0.9"; + const resolveActive = document.createElement("div"); + resolveActive.style.opacity = "0.85"; + const resolveBtns = document.createElement("div"); + resolveBtns.style.display = "flex"; + resolveBtns.style.gap = UI_GAP; + resolveBtns.style.flexWrap = "wrap"; + resolveBtns.style.justifyContent = "center"; + resolveBtns.style.alignItems = "center"; + resolveBtns.style.marginTop = "0"; + const switchBtn = btn("Switch to detected project"); + const assignBtn = btn("Assign to active project"); + const createBtn = btn("Create new project"); + const dismissBtn = btn("Dismiss"); + for (const b of [switchBtn, assignBtn, createBtn, dismissBtn]) { + b.style.fontSize = "11px"; + b.style.padding = "6px 8px"; + b.style.margin = "0"; + } + resolveBtns.appendChild(switchBtn); + resolveBtns.appendChild(assignBtn); + resolveBtns.appendChild(createBtn); + resolveBtns.appendChild(dismissBtn); + resolveWrap.appendChild(resolveTitle); + resolveWrap.appendChild(resolveInfo); + resolveWrap.appendChild(resolveActive); + resolveWrap.appendChild(resolveBtns); + const workflowWrap = document.createElement("div"); + workflowWrap.style.display = "none"; + workflowWrap.style.flexDirection = "column"; + workflowWrap.style.gap = UI_GAP; + workflowWrap.style.padding = "8px 10px"; + workflowWrap.style.borderRadius = "8px"; + workflowWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowWrap.style.backdropFilter = "blur(6px)"; + const workflowTitle = document.createElement("div"); + workflowTitle.textContent = "Workflow Project"; + workflowTitle.style.textTransform = "uppercase"; + workflowTitle.style.letterSpacing = "0.6px"; + workflowTitle.style.fontWeight = "600"; + workflowTitle.style.color = "#8fb9ff"; + workflowTitle.style.marginBottom = "0"; + const pendingProjectNameInput = document.createElement("input"); + pendingProjectNameInput.type = "text"; + pendingProjectNameInput.placeholder = "Project name"; + pendingProjectNameInput.value = state.pendingProjectName || ""; + const pendingSelect = document.createElement("select"); + pendingSelect.className = "mjr-ps-select"; + pendingSelect.style.width = "100%"; + pendingSelect.style.padding = "6px 8px"; + pendingSelect.style.borderRadius = "8px"; + pendingSelect.style.border = "1px solid rgba(255,255,255,0.12)"; + pendingSelect.style.color = "#f2f4f8"; + const wfCreateBtn = btn("Create & Activate"); + const wfActivateBtn = btn("Activate Selected"); + const wfAssignBtn = btn("Assign workflow to active"); + for (const b of [wfCreateBtn, wfActivateBtn, wfAssignBtn]) { + b.style.fontSize = "11px"; + b.style.padding = "6px 8px"; + b.style.margin = "0"; + } + const wfButtons = document.createElement("div"); + wfButtons.style.display = "flex"; + wfButtons.style.gap = UI_GAP; + wfButtons.style.flexWrap = "wrap"; + wfButtons.style.justifyContent = "center"; + wfButtons.style.alignItems = "center"; + wfButtons.style.marginTop = "0"; + wfButtons.appendChild(wfCreateBtn); + wfButtons.appendChild(wfActivateBtn); + wfButtons.appendChild(wfAssignBtn); + workflowWrap.appendChild(workflowTitle); + workflowWrap.appendChild(row("Project name", pendingProjectNameInput)); + workflowWrap.appendChild(row("Or select existing project", pendingSelect)); + workflowWrap.appendChild(wfButtons); + let refreshProjectsList = () => { }; + const updateWorkflowBlock = () => { + const show = !!state.workflowPanelOpen; + workflowWrap.style.display = show ? "flex" : "none"; + if (show) { + refreshProjectsList(); + pendingProjectNameInput.value = state.pendingProjectName || ""; + if (state.pendingSelectProjectId) { + pendingSelect.value = state.pendingSelectProjectId; + } + } + }; + const autoPatchNewWrap = document.createElement("label"); + autoPatchNewWrap.style.display = "flex"; + autoPatchNewWrap.style.alignItems = "center"; + autoPatchNewWrap.style.gap = UI_GAP; + autoPatchNewWrap.style.cursor = "pointer"; + const autoPatchNewCb = document.createElement("input"); + autoPatchNewCb.type = "checkbox"; + autoPatchNewCb.checked = state.autoPatchNewNodes !== false; + const autoPatchNewText = document.createElement("span"); + autoPatchNewText.textContent = "Auto patch new nodes"; + autoPatchNewText.style.opacity = "0.9"; + autoPatchNewWrap.appendChild(autoPatchNewCb); + autoPatchNewWrap.appendChild(autoPatchNewText); + const autoSwitchWrap = document.createElement("label"); + autoSwitchWrap.style.display = "flex"; + autoSwitchWrap.style.alignItems = "center"; + autoSwitchWrap.style.gap = UI_GAP; + autoSwitchWrap.style.cursor = "pointer"; + const autoSwitchCb = document.createElement("input"); + autoSwitchCb.type = "checkbox"; + autoSwitchCb.checked = state.autoSwitchTrusted === true; + const autoSwitchText = document.createElement("span"); + autoSwitchText.textContent = "Auto-switch trusted signature"; + autoSwitchText.style.opacity = "0.9"; + autoSwitchWrap.appendChild(autoSwitchCb); + autoSwitchWrap.appendChild(autoSwitchText); + // Model section (collapsible) + const modelSection = detailsSection("Model"); + const modelSummary = modelSection._summary; + const detectedModelLabel = document.createElement("div"); + detectedModelLabel.style.opacity = "0.9"; + detectedModelLabel.style.fontSize = "11px"; + detectedModelLabel.style.color = "#f2d98a"; + detectedModelLabel.style.textAlign = "left"; + detectedModelLabel.style.marginLeft = "2px"; + detectedModelLabel.style.marginTop = "0"; + detectedModelLabel.textContent = "Detected: -"; + const useCustomWrap = document.createElement("label"); + useCustomWrap.style.display = "flex"; + useCustomWrap.style.alignItems = "center"; + useCustomWrap.style.gap = UI_GAP; + useCustomWrap.style.cursor = "pointer"; + const useCustomCb = document.createElement("input"); + useCustomCb.type = "checkbox"; + useCustomCb.checked = state.useCustomModel === true; + const useCustomText = document.createElement("span"); + useCustomText.textContent = "Use custom model (UPPERCASE)"; + useCustomText.style.opacity = "0.9"; + useCustomWrap.appendChild(useCustomCb); + useCustomWrap.appendChild(useCustomText); + const modelSelect = document.createElement("select"); + modelSelect.className = "mjr-ps-select"; + modelSelect.style.width = "100%"; + modelSelect.style.padding = "6px 8px"; + modelSelect.style.borderRadius = "8px"; + modelSelect.style.border = "1px solid rgba(255,255,255,0.12)"; + modelSelect.style.color = "#f2f4f8"; + const modelRow = document.createElement("div"); + modelRow.style.display = "flex"; + modelRow.style.gap = UI_GAP; + const modelToggleBtn = btn(state.showMoreModels ? "Show less" : "Show more"); + modelToggleBtn.style.padding = "4px 6px"; + modelToggleBtn.style.fontSize = "10px"; + modelToggleBtn.style.flex = "0 0 auto"; + modelToggleBtn.style.margin = "0"; + modelToggleBtn.style.alignSelf = "flex-end"; + const modelSelectWrap = row("Default Model", modelSelect); + modelSelectWrap.style.flex = "1 1 auto"; + modelRow.appendChild(modelSelectWrap); + const modelEmptyMsg = document.createElement("div"); + modelEmptyMsg.style.opacity = "0.8"; + modelEmptyMsg.style.display = "none"; + modelEmptyMsg.style.textAlign = "center"; + modelEmptyMsg.style.fontSize = "11px"; + modelEmptyMsg.style.color = "#f2d98a"; + modelEmptyMsg.textContent = "No models detected (check folders)"; + const customModelInput = document.createElement("input"); + customModelInput.type = "text"; + customModelInput.placeholder = "Custom model name"; + customModelInput.value = state.customModelText || ""; + const customModelWrap = row("Custom Model (UPPERCASE)", customModelInput); + customModelWrap.style.display = state.useCustomModel ? "block" : "none"; + customModelWrap.style.flex = "1 1 auto"; + const customModelPreview = document.createElement("div"); + customModelPreview.style.opacity = "0.85"; + customModelPreview.style.fontSize = "9px"; + customModelPreview.style.color = "#f2d98a"; + customModelPreview.style.textAlign = "center"; + customModelPreview.textContent = "Custom tag: -"; + customModelWrap.appendChild(customModelPreview); + const modelCustomRow = document.createElement("div"); + modelCustomRow.style.display = "flex"; + modelCustomRow.style.gap = UI_GAP; + modelCustomRow.style.alignItems = "flex-end"; + modelCustomRow.appendChild(customModelWrap); + modelCustomRow.appendChild(modelToggleBtn); + modelSection.appendChild(detectedModelLabel); + modelSection.appendChild(useCustomWrap); + modelSection.appendChild(modelRow); + modelSection.appendChild(modelCustomRow); + modelSection.appendChild(modelEmptyMsg); + // Workflow section + const workflowNameInput = document.createElement("input"); + workflowNameInput.type = "text"; + workflowNameInput.placeholder = "Workflow name (optional)"; + workflowNameInput.value = state.workflowName || ""; + const workflowAssetInput = document.createElement("input"); + workflowAssetInput.type = "text"; + workflowAssetInput.placeholder = "Asset folder (optional)"; + workflowAssetInput.value = state.workflowAsset || ""; + const workflowPreviewLabel = document.createElement("div"); + workflowPreviewLabel.style.opacity = "0.85"; + workflowPreviewLabel.style.textAlign = "left"; + workflowPreviewLabel.style.fontSize = "9px"; + workflowPreviewLabel.style.color = "#f2d98a"; + workflowPreviewLabel.style.lineHeight = "1.35"; + workflowPreviewLabel.style.wordBreak = "break-word"; + workflowPreviewLabel.textContent = "Next save: -"; + const saveWorkflowBtn = btn("Save Workflow to Project"); + saveWorkflowBtn.style.alignSelf = "center"; + saveWorkflowBtn.style.margin = "0 auto"; + saveWorkflowBtn.style.display = "block"; + const autoHookWrap = document.createElement("label"); + autoHookWrap.style.display = "flex"; + autoHookWrap.style.alignItems = "center"; + autoHookWrap.style.gap = UI_GAP; + autoHookWrap.style.cursor = "pointer"; + const autoHookCb = document.createElement("input"); + autoHookCb.type = "checkbox"; + autoHookCb.checked = state.autoHookSave !== false; + const autoHookText = document.createElement("span"); + autoHookText.textContent = "Auto-hook Ctrl+S"; + autoHookText.style.opacity = "0.9"; + autoHookWrap.appendChild(autoHookCb); + autoHookWrap.appendChild(autoHookText); + // Custom output section (collapsible) + const customOutputSection = detailsSection("Custom Output (optional)"); + customOutputSection.style.display = "flex"; + customOutputSection.style.flexDirection = "column"; + customOutputSection.style.gap = UI_GAP; + const kindSelect = document.createElement("select"); + kindSelect.className = "mjr-ps-select"; + for (const [value, label] of [ + ["asset", "Asset"], + ["shot", "Shot"], + ]) { + const opt = document.createElement("option"); + opt.value = value; + opt.textContent = label; + kindSelect.appendChild(opt); + } + kindSelect.value = state.kind || "asset"; + const mediaSelect = document.createElement("select"); + mediaSelect.className = "mjr-ps-select"; + for (const [value, label] of [ + ["images", "Images"], + ["videos", "Videos"], + ]) { + const opt = document.createElement("option"); + opt.value = value; + opt.textContent = label; + mediaSelect.appendChild(opt); + } + mediaSelect.value = state.media || "images"; + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.placeholder = "Asset/shot name"; + nameInput.value = state.name || ""; + styleField(nameInput); + const existingNamesSelect = document.createElement("select"); + existingNamesSelect.className = "mjr-ps-select"; + existingNamesSelect.style.display = "none"; + styleField(existingNamesSelect); + const nameStack = document.createElement("div"); + nameStack.style.display = "flex"; + nameStack.style.flexDirection = "column"; + nameStack.style.gap = UI_GAP; + nameStack.style.width = "100%"; + nameStack.appendChild(existingNamesSelect); + nameStack.appendChild(nameInput); + const autoPatchWrap = document.createElement("label"); + autoPatchWrap.style.display = "flex"; + autoPatchWrap.style.alignItems = "center"; + autoPatchWrap.style.gap = UI_GAP; + autoPatchWrap.style.cursor = "pointer"; + const autoPatchCb = document.createElement("input"); + autoPatchCb.type = "checkbox"; + autoPatchCb.checked = state.autoPatch !== false; + const autoPatchText = document.createElement("span"); + autoPatchText.textContent = "Auto patch outputs"; + autoPatchText.style.opacity = "0.9"; + autoPatchWrap.appendChild(autoPatchCb); + autoPatchWrap.appendChild(autoPatchText); + const createApplyBtn = btn("Create + Apply"); + const patchNowBtn = btn("Patch now"); + const buttonRow = document.createElement("div"); + buttonRow.style.display = "flex"; + buttonRow.style.gap = UI_GAP; + buttonRow.style.justifyContent = "center"; + buttonRow.style.alignItems = "center"; + buttonRow.style.marginTop = "0"; + buttonRow.style.marginBottom = "0"; + buttonRow.appendChild(createApplyBtn); + buttonRow.appendChild(patchNowBtn); + const targetLabel = document.createElement("div"); + targetLabel.style.opacity = "0.85"; + targetLabel.style.textAlign = "left"; + targetLabel.style.fontSize = "9px"; + targetLabel.style.color = "#f2d98a"; + targetLabel.style.lineHeight = "1.35"; + targetLabel.style.wordBreak = "break-word"; + const updateTargetLabel = () => { + if (state.lastRelDir) { + targetLabel.textContent = `Target: ${state.lastRelDir} | Prefix: ${state.lastPrefix}`; + } + else { + targetLabel.textContent = "Target: none (create an output)"; + } + }; + const rollOutputDateIfNeeded = () => { + const today = yymmddJS(); + const prefix = String(state.lastPrefix || ""); + const m = prefix.match(/^(\d{6})_/); + const oldDate = (m ? m[1] : "") || + String(state.lastRelDir || "").replace(/\\/g, "/").match(/(?:^|\/)(\d{6})(?:\/|$)/)?.[1] || + ""; + if (!oldDate || oldDate === today) + return false; + if (String(state.lastRelDir || "")) { + const rel = String(state.lastRelDir || "").replace(/\\/g, "/"); + const re = new RegExp(`(^|/)${oldDate}(/|$)`, "g"); + state.lastRelDir = rel.replace(re, `$1${today}$2`); + } + if (prefix.startsWith(`${oldDate}_`)) { + state.lastPrefix = `${today}${prefix.slice(6)}`; + } + saveState(state); + updateTargetLabel(); + return true; + }; + rollOutputDateIfNeeded(); + updateTargetLabel(); + // Advanced template + const advanced = document.createElement("details"); + advanced.style.marginTop = "0"; + advanced.style.display = "flex"; + advanced.style.flexDirection = "column"; + advanced.style.gap = UI_GAP; const summary = document.createElement("summary"); - summary.textContent = title; + summary.textContent = "Advanced"; summary.style.cursor = "pointer"; - summary.style.opacity = "0.9"; + summary.style.opacity = "0.85"; summary.style.textTransform = "uppercase"; summary.style.letterSpacing = "0.6px"; summary.style.color = "#8fb9ff"; - summary.style.fontWeight = "600"; - summary.style.paddingLeft = "2px"; - details.appendChild(summary); - details._summary = summary; - return details; - }; - - const makeLabel = (labelText) => { - const label = document.createElement("label"); - label.textContent = labelText; - label.style.opacity = "0.85"; - label.style.marginLeft = "2px"; - label.style.fontSize = "11px"; - label.style.lineHeight = "1.2"; - return label; - }; - - const styleField = (inputEl) => { - inputEl.style.width = "100%"; - inputEl.style.boxSizing = "border-box"; - inputEl.style.padding = "6px 8px"; - inputEl.style.borderRadius = "8px"; - inputEl.style.border = "1px solid rgba(255,255,255,0.12)"; - inputEl.style.background = "rgba(0,0,0,0.25)"; - inputEl.style.color = "#eee"; - inputEl.style.outline = "none"; - }; - - const row = (labelText, inputEl) => { - const wrap = document.createElement("div"); - wrap.style.display = "flex"; - wrap.style.flexDirection = "column"; - wrap.style.gap = UI_GAP; - wrap.appendChild(makeLabel(labelText)); - styleField(inputEl); - wrap.appendChild(inputEl); - return wrap; - }; - - const groupRow = (labelText, contentEl) => { - const wrap = document.createElement("div"); - wrap.style.display = "flex"; - wrap.style.flexDirection = "column"; - wrap.style.gap = UI_GAP; - wrap.appendChild(makeLabel(labelText)); - wrap.appendChild(contentEl); - return wrap; - }; - - const btn = (text) => { - const b = document.createElement("button"); - b.textContent = text; - b.className = "mjr-ps-btn"; - b.style.padding = "7px 8px"; - b.style.borderRadius = "8px"; - b.style.border = "1px solid rgba(46, 136, 255, 0.65)"; - b.style.background = "rgba(46, 136, 255, 0.25)"; - b.style.color = "#eee"; - b.style.cursor = "pointer"; - b.style.margin = "0"; - b.style.lineHeight = "1.2"; - b.style.whiteSpace = "nowrap"; - return b; - }; - - // Status bar - const statusBar = document.createElement("div"); - statusBar.style.display = "flex"; - statusBar.style.alignItems = "center"; - statusBar.style.gap = UI_GAP; - statusBar.style.padding = "6px 8px"; - statusBar.style.borderRadius = "8px"; - statusBar.style.position = "sticky"; - statusBar.style.top = "0"; - statusBar.style.zIndex = "2"; - statusBar.style.background = "rgba(10, 12, 16, 0.72)"; - statusBar.style.backdropFilter = "blur(6px)"; - statusBar.style.cursor = "pointer"; - statusBar.title = "Toggle workflow project panel"; - const statusDot = document.createElement("div"); - statusDot.style.width = "8px"; - statusDot.style.height = "8px"; - statusDot.style.borderRadius = "50%"; - const statusText = document.createElement("div"); - statusText.style.opacity = "0.95"; - - const updateStatus = () => { - const s = makeStatus(state); - const level = s.level || "gray"; - statusBar.style.background = colorWithAlpha(s.color, 0.92); - statusBar.style.border = `1px solid ${colorWithAlpha(s.color, 0.35)}`; - statusDot.style.background = s.color; - statusText.textContent = s.text; - if (workflowSaveWrap) { - workflowSaveWrap.style.border = `1px solid ${colorWithAlpha(s.color, 0.4)}`; - workflowSaveWrap.style.background = colorWithAlpha(s.color, 0.12); - } - if (workflowWrap) { - const emphasize = level === "orange" && state.workflowChecked && !state.workflowHasSignature; - workflowWrap.style.border = emphasize - ? `1px solid ${colorWithAlpha("#c77c2a", 0.7)}` - : "1px solid rgba(255,255,255,0.12)"; - workflowWrap.style.background = emphasize - ? colorWithAlpha("#c77c2a", 0.1) - : "rgba(15, 17, 22, 0.75)"; - } - updateMenuStatusBadge(state); - }; - statusBar.appendChild(statusDot); - statusBar.appendChild(statusText); - statusBar.addEventListener("click", () => { - state.workflowPanelOpen = !state.workflowPanelOpen; - saveState(state); - updateWorkflowBlock(); - }); - - const updateResolve = () => { - const det = state.detected; - let show = true; - if (!det || det.mode === "none") { - show = false; - } - const sameProject = - det && - det.mode !== "none" && - ((det.project_id && det.project_id === state.projectId) || - (det.project_folder && det.project_folder === state.projectFolder)); - if (sameProject) { - show = false; - } - if (!show) { - resolveWrap.style.display = "none"; - return; - } - - if (state.workflowPanelOpen) { - state.workflowPanelOpen = false; - saveState(state); - } - - const mode = det.mode || "none"; - const wfFolder = det.project_folder || "none"; - resolveInfo.textContent = `Workflow project: ${wfFolder} (mode: ${mode})`; - resolveActive.textContent = `Active project: ${state.projectFolder || "none"}`; - resolveWrap.style.display = "flex"; - - const canSwitch = mode !== "none" && (det.project_id || det.project_folder); - switchBtn.disabled = !canSwitch; - switchBtn.style.opacity = canSwitch ? "1" : "0.5"; - - const canAssign = !!state.projectId; - assignBtn.disabled = !canAssign; - assignBtn.style.opacity = canAssign ? "1" : "0.5"; - }; - - const resolveWrap = document.createElement("div"); - resolveWrap.style.display = "none"; - resolveWrap.style.flexDirection = "column"; - resolveWrap.style.gap = UI_GAP; - resolveWrap.style.padding = "8px 10px"; - resolveWrap.style.borderRadius = "8px"; - resolveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; - resolveWrap.style.background = "rgba(15, 17, 22, 0.75)"; - resolveWrap.style.backdropFilter = "blur(6px)"; - - const resolveTitle = document.createElement("div"); - resolveTitle.textContent = "Workflow project"; - resolveTitle.style.textTransform = "uppercase"; - resolveTitle.style.letterSpacing = "0.6px"; - resolveTitle.style.fontWeight = "600"; - resolveTitle.style.color = "#8fb9ff"; - resolveTitle.style.marginBottom = "0"; - - const resolveInfo = document.createElement("div"); - resolveInfo.style.opacity = "0.9"; - - const resolveActive = document.createElement("div"); - resolveActive.style.opacity = "0.85"; - - const resolveBtns = document.createElement("div"); - resolveBtns.style.display = "flex"; - resolveBtns.style.gap = UI_GAP; - resolveBtns.style.flexWrap = "wrap"; - resolveBtns.style.justifyContent = "center"; - resolveBtns.style.alignItems = "center"; - resolveBtns.style.marginTop = "0"; - - const switchBtn = btn("Switch to detected project"); - const assignBtn = btn("Assign to active project"); - const createBtn = btn("Create new project"); - const dismissBtn = btn("Dismiss"); - for (const b of [switchBtn, assignBtn, createBtn, dismissBtn]) { - b.style.fontSize = "11px"; - b.style.padding = "6px 8px"; - b.style.margin = "0"; - } - resolveBtns.appendChild(switchBtn); - resolveBtns.appendChild(assignBtn); - resolveBtns.appendChild(createBtn); - resolveBtns.appendChild(dismissBtn); - - resolveWrap.appendChild(resolveTitle); - resolveWrap.appendChild(resolveInfo); - resolveWrap.appendChild(resolveActive); - resolveWrap.appendChild(resolveBtns); - - const workflowWrap = document.createElement("div"); - workflowWrap.style.display = "none"; - workflowWrap.style.flexDirection = "column"; - workflowWrap.style.gap = UI_GAP; - workflowWrap.style.padding = "8px 10px"; - workflowWrap.style.borderRadius = "8px"; - workflowWrap.style.border = "1px solid rgba(255,255,255,0.12)"; - workflowWrap.style.background = "rgba(15, 17, 22, 0.75)"; - workflowWrap.style.backdropFilter = "blur(6px)"; - - const workflowTitle = document.createElement("div"); - workflowTitle.textContent = "Workflow Project"; - workflowTitle.style.textTransform = "uppercase"; - workflowTitle.style.letterSpacing = "0.6px"; - workflowTitle.style.fontWeight = "600"; - workflowTitle.style.color = "#8fb9ff"; - workflowTitle.style.marginBottom = "0"; - - const pendingProjectNameInput = document.createElement("input"); - pendingProjectNameInput.type = "text"; - pendingProjectNameInput.placeholder = "Project name"; - pendingProjectNameInput.value = state.pendingProjectName || ""; - - const pendingSelect = document.createElement("select"); - pendingSelect.className = "mjr-ps-select"; - pendingSelect.style.width = "100%"; - pendingSelect.style.padding = "6px 8px"; - pendingSelect.style.borderRadius = "8px"; - pendingSelect.style.border = "1px solid rgba(255,255,255,0.12)"; - pendingSelect.style.color = "#f2f4f8"; - - const wfCreateBtn = btn("Create & Activate"); - const wfActivateBtn = btn("Activate Selected"); - const wfAssignBtn = btn("Assign workflow to active"); - for (const b of [wfCreateBtn, wfActivateBtn, wfAssignBtn]) { - b.style.fontSize = "11px"; - b.style.padding = "6px 8px"; - b.style.margin = "0"; - } - - const wfButtons = document.createElement("div"); - wfButtons.style.display = "flex"; - wfButtons.style.gap = UI_GAP; - wfButtons.style.flexWrap = "wrap"; - wfButtons.style.justifyContent = "center"; - wfButtons.style.alignItems = "center"; - wfButtons.style.marginTop = "0"; - wfButtons.appendChild(wfCreateBtn); - wfButtons.appendChild(wfActivateBtn); - wfButtons.appendChild(wfAssignBtn); - - workflowWrap.appendChild(workflowTitle); - workflowWrap.appendChild(row("Project name", pendingProjectNameInput)); - workflowWrap.appendChild(row("Or select existing project", pendingSelect)); - workflowWrap.appendChild(wfButtons); - - let refreshProjectsList = () => {}; - - const updateWorkflowBlock = () => { - const show = !!state.workflowPanelOpen; - workflowWrap.style.display = show ? "flex" : "none"; - if (show) { - refreshProjectsList(); - pendingProjectNameInput.value = state.pendingProjectName || ""; - if (state.pendingSelectProjectId) { - pendingSelect.value = state.pendingSelectProjectId; - } - } - }; - - const autoPatchNewWrap = document.createElement("label"); - autoPatchNewWrap.style.display = "flex"; - autoPatchNewWrap.style.alignItems = "center"; - autoPatchNewWrap.style.gap = UI_GAP; - autoPatchNewWrap.style.cursor = "pointer"; - const autoPatchNewCb = document.createElement("input"); - autoPatchNewCb.type = "checkbox"; - autoPatchNewCb.checked = state.autoPatchNewNodes !== false; - const autoPatchNewText = document.createElement("span"); - autoPatchNewText.textContent = "Auto patch new nodes"; - autoPatchNewText.style.opacity = "0.9"; - autoPatchNewWrap.appendChild(autoPatchNewCb); - autoPatchNewWrap.appendChild(autoPatchNewText); - - const autoSwitchWrap = document.createElement("label"); - autoSwitchWrap.style.display = "flex"; - autoSwitchWrap.style.alignItems = "center"; - autoSwitchWrap.style.gap = UI_GAP; - autoSwitchWrap.style.cursor = "pointer"; - const autoSwitchCb = document.createElement("input"); - autoSwitchCb.type = "checkbox"; - autoSwitchCb.checked = state.autoSwitchTrusted === true; - const autoSwitchText = document.createElement("span"); - autoSwitchText.textContent = "Auto-switch trusted signature"; - autoSwitchText.style.opacity = "0.9"; - autoSwitchWrap.appendChild(autoSwitchCb); - autoSwitchWrap.appendChild(autoSwitchText); - - // Model section (collapsible) - const modelSection = detailsSection("Model"); - const modelSummary = modelSection._summary; - - const detectedModelLabel = document.createElement("div"); - detectedModelLabel.style.opacity = "0.9"; - detectedModelLabel.style.fontSize = "11px"; - detectedModelLabel.style.color = "#f2d98a"; - detectedModelLabel.style.textAlign = "left"; - detectedModelLabel.style.marginLeft = "2px"; - detectedModelLabel.style.marginTop = "0"; - detectedModelLabel.textContent = "Detected: -"; - - const useCustomWrap = document.createElement("label"); - useCustomWrap.style.display = "flex"; - useCustomWrap.style.alignItems = "center"; - useCustomWrap.style.gap = UI_GAP; - useCustomWrap.style.cursor = "pointer"; - const useCustomCb = document.createElement("input"); - useCustomCb.type = "checkbox"; - useCustomCb.checked = state.useCustomModel === true; - const useCustomText = document.createElement("span"); - useCustomText.textContent = "Use custom model (UPPERCASE)"; - useCustomText.style.opacity = "0.9"; - useCustomWrap.appendChild(useCustomCb); - useCustomWrap.appendChild(useCustomText); - - const modelSelect = document.createElement("select"); - modelSelect.className = "mjr-ps-select"; - modelSelect.style.width = "100%"; - modelSelect.style.padding = "6px 8px"; - modelSelect.style.borderRadius = "8px"; - modelSelect.style.border = "1px solid rgba(255,255,255,0.12)"; - modelSelect.style.color = "#f2f4f8"; - - const modelRow = document.createElement("div"); - modelRow.style.display = "flex"; - modelRow.style.gap = UI_GAP; - const modelToggleBtn = btn(state.showMoreModels ? "Show less" : "Show more"); - modelToggleBtn.style.padding = "4px 6px"; - modelToggleBtn.style.fontSize = "10px"; - modelToggleBtn.style.flex = "0 0 auto"; - modelToggleBtn.style.margin = "0"; - modelToggleBtn.style.alignSelf = "flex-end"; - const modelSelectWrap = row("Default Model", modelSelect); - modelSelectWrap.style.flex = "1 1 auto"; - modelRow.appendChild(modelSelectWrap); - - const modelEmptyMsg = document.createElement("div"); - modelEmptyMsg.style.opacity = "0.8"; - modelEmptyMsg.style.display = "none"; - modelEmptyMsg.style.textAlign = "center"; - modelEmptyMsg.style.fontSize = "11px"; - modelEmptyMsg.style.color = "#f2d98a"; - modelEmptyMsg.textContent = "No models detected (check folders)"; - - const customModelInput = document.createElement("input"); - customModelInput.type = "text"; - customModelInput.placeholder = "Custom model name"; - customModelInput.value = state.customModelText || ""; - - const customModelWrap = row("Custom Model (UPPERCASE)", customModelInput); - customModelWrap.style.display = state.useCustomModel ? "block" : "none"; - customModelWrap.style.flex = "1 1 auto"; - - const customModelPreview = document.createElement("div"); - customModelPreview.style.opacity = "0.85"; - customModelPreview.style.fontSize = "9px"; - customModelPreview.style.color = "#f2d98a"; - customModelPreview.style.textAlign = "center"; - customModelPreview.textContent = "Custom tag: -"; - customModelWrap.appendChild(customModelPreview); - - const modelCustomRow = document.createElement("div"); - modelCustomRow.style.display = "flex"; - modelCustomRow.style.gap = UI_GAP; - modelCustomRow.style.alignItems = "flex-end"; - modelCustomRow.appendChild(customModelWrap); - modelCustomRow.appendChild(modelToggleBtn); - - modelSection.appendChild(detectedModelLabel); - modelSection.appendChild(useCustomWrap); - modelSection.appendChild(modelRow); - modelSection.appendChild(modelCustomRow); - modelSection.appendChild(modelEmptyMsg); - - // Workflow section - const workflowNameInput = document.createElement("input"); - workflowNameInput.type = "text"; - workflowNameInput.placeholder = "Workflow name (optional)"; - workflowNameInput.value = state.workflowName || ""; - - const workflowAssetInput = document.createElement("input"); - workflowAssetInput.type = "text"; - workflowAssetInput.placeholder = "Asset folder (optional)"; - workflowAssetInput.value = state.workflowAsset || ""; - - const workflowPreviewLabel = document.createElement("div"); - workflowPreviewLabel.style.opacity = "0.85"; - workflowPreviewLabel.style.textAlign = "left"; - workflowPreviewLabel.style.fontSize = "9px"; - workflowPreviewLabel.style.color = "#f2d98a"; - workflowPreviewLabel.style.lineHeight = "1.35"; - workflowPreviewLabel.style.wordBreak = "break-word"; - workflowPreviewLabel.textContent = "Next save: -"; - - const saveWorkflowBtn = btn("Save Workflow to Project"); - saveWorkflowBtn.style.alignSelf = "center"; - saveWorkflowBtn.style.margin = "0 auto"; - saveWorkflowBtn.style.display = "block"; - - const autoHookWrap = document.createElement("label"); - autoHookWrap.style.display = "flex"; - autoHookWrap.style.alignItems = "center"; - autoHookWrap.style.gap = UI_GAP; - autoHookWrap.style.cursor = "pointer"; - const autoHookCb = document.createElement("input"); - autoHookCb.type = "checkbox"; - autoHookCb.checked = state.autoHookSave !== false; - const autoHookText = document.createElement("span"); - autoHookText.textContent = "Auto-hook Ctrl+S"; - autoHookText.style.opacity = "0.9"; - autoHookWrap.appendChild(autoHookCb); - autoHookWrap.appendChild(autoHookText); - - // Custom output section (collapsible) - const customOutputSection = detailsSection("Custom Output (optional)"); - customOutputSection.style.display = "flex"; - customOutputSection.style.flexDirection = "column"; - customOutputSection.style.gap = UI_GAP; - - const kindSelect = document.createElement("select"); - kindSelect.className = "mjr-ps-select"; - for (const [value, label] of [ - ["asset", "Asset"], - ["shot", "Shot"], - ]) { - const opt = document.createElement("option"); - opt.value = value; - opt.textContent = label; - kindSelect.appendChild(opt); - } - kindSelect.value = state.kind || "asset"; - - const mediaSelect = document.createElement("select"); - mediaSelect.className = "mjr-ps-select"; - for (const [value, label] of [ - ["images", "Images"], - ["videos", "Videos"], - ]) { - const opt = document.createElement("option"); - opt.value = value; - opt.textContent = label; - mediaSelect.appendChild(opt); - } - mediaSelect.value = state.media || "images"; - - const nameInput = document.createElement("input"); - nameInput.type = "text"; - nameInput.placeholder = "Asset/shot name"; - nameInput.value = state.name || ""; - styleField(nameInput); - - const existingNamesSelect = document.createElement("select"); - existingNamesSelect.className = "mjr-ps-select"; - existingNamesSelect.style.display = "none"; - styleField(existingNamesSelect); - - const nameStack = document.createElement("div"); - nameStack.style.display = "flex"; - nameStack.style.flexDirection = "column"; - nameStack.style.gap = UI_GAP; - nameStack.style.width = "100%"; - nameStack.appendChild(existingNamesSelect); - nameStack.appendChild(nameInput); - - const autoPatchWrap = document.createElement("label"); - autoPatchWrap.style.display = "flex"; - autoPatchWrap.style.alignItems = "center"; - autoPatchWrap.style.gap = UI_GAP; - autoPatchWrap.style.cursor = "pointer"; - const autoPatchCb = document.createElement("input"); - autoPatchCb.type = "checkbox"; - autoPatchCb.checked = state.autoPatch !== false; - const autoPatchText = document.createElement("span"); - autoPatchText.textContent = "Auto patch outputs"; - autoPatchText.style.opacity = "0.9"; - autoPatchWrap.appendChild(autoPatchCb); - autoPatchWrap.appendChild(autoPatchText); - - const createApplyBtn = btn("Create + Apply"); - const patchNowBtn = btn("Patch now"); - - const buttonRow = document.createElement("div"); - buttonRow.style.display = "flex"; - buttonRow.style.gap = UI_GAP; - buttonRow.style.justifyContent = "center"; - buttonRow.style.alignItems = "center"; - buttonRow.style.marginTop = "0"; - buttonRow.style.marginBottom = "0"; - buttonRow.appendChild(createApplyBtn); - buttonRow.appendChild(patchNowBtn); - - const targetLabel = document.createElement("div"); - targetLabel.style.opacity = "0.85"; - targetLabel.style.textAlign = "left"; - targetLabel.style.fontSize = "9px"; - targetLabel.style.color = "#f2d98a"; - targetLabel.style.lineHeight = "1.35"; - targetLabel.style.wordBreak = "break-word"; - - const updateTargetLabel = () => { - if (state.lastRelDir) { - targetLabel.textContent = `Target: ${state.lastRelDir} | Prefix: ${state.lastPrefix}`; - } else { - targetLabel.textContent = "Target: none (create an output)"; - } - }; - - const rollOutputDateIfNeeded = () => { - const today = yymmddJS(); - const prefix = String(state.lastPrefix || ""); - const m = prefix.match(/^(\d{6})_/); - const oldDate = - (m ? m[1] : "") || - String(state.lastRelDir || "").replace(/\\/g, "/").match(/(?:^|\/)(\d{6})(?:\/|$)/)?.[1] || - ""; - - if (!oldDate || oldDate === today) return false; - - if (String(state.lastRelDir || "")) { - const rel = String(state.lastRelDir || "").replace(/\\/g, "/"); - const re = new RegExp(`(^|/)${oldDate}(/|$)`, "g"); - state.lastRelDir = rel.replace(re, `$1${today}$2`); - } - - if (prefix.startsWith(`${oldDate}_`)) { - state.lastPrefix = `${today}${prefix.slice(6)}`; - } - - saveState(state); - updateTargetLabel(); - return true; - }; - - rollOutputDateIfNeeded(); - updateTargetLabel(); - - // Advanced template - const advanced = document.createElement("details"); - advanced.style.marginTop = "0"; - advanced.style.display = "flex"; - advanced.style.flexDirection = "column"; - advanced.style.gap = UI_GAP; - const summary = document.createElement("summary"); - summary.textContent = "Advanced"; - summary.style.cursor = "pointer"; - summary.style.opacity = "0.85"; - summary.style.textTransform = "uppercase"; - summary.style.letterSpacing = "0.6px"; - summary.style.color = "#8fb9ff"; - - const templateInput = document.createElement("input"); - templateInput.type = "text"; - templateInput.placeholder = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; - templateInput.value = - state.pathTemplate || "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; - - const templateTokens = document.createElement("div"); - templateTokens.style.opacity = "0.85"; - templateTokens.style.fontSize = "8px"; - templateTokens.style.color = "#f2d98a"; - templateTokens.style.textAlign = "left"; - templateTokens.style.lineHeight = "1.35"; - templateTokens.style.wordBreak = "break-word"; - templateTokens.textContent = `Tokens: ${TEMPLATE_TOKENS.join(" ")}`; - - const previewLabel = document.createElement("div"); - previewLabel.style.opacity = "0.85"; - previewLabel.style.fontSize = "8px"; - previewLabel.style.color = "#f2d98a"; - previewLabel.style.textAlign = "left"; - previewLabel.style.lineHeight = "1.35"; - previewLabel.style.wordBreak = "break-word"; - previewLabel.textContent = "Preview: -"; - - advanced.appendChild(summary); - advanced.appendChild(row("Path Template", templateInput)); - advanced.appendChild(templateTokens); - advanced.appendChild(previewLabel); - - const workflowSaveWrap = detailsSection("Workflow"); - workflowSaveWrap.open = true; - workflowSaveWrap.style.padding = "8px 10px"; - workflowSaveWrap.style.borderRadius = "8px"; - workflowSaveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; - workflowSaveWrap.style.background = "rgba(15, 17, 22, 0.75)"; - workflowSaveWrap.style.backdropFilter = "blur(6px)"; - const workflowSaveContent = document.createElement("div"); - workflowSaveContent.style.display = "flex"; - workflowSaveContent.style.flexDirection = "column"; - workflowSaveContent.style.gap = UI_GAP; - workflowSaveContent.appendChild(row("Workflow name", workflowNameInput)); - workflowSaveContent.appendChild(row("Asset folder", workflowAssetInput)); - workflowSaveContent.appendChild(workflowPreviewLabel); - workflowSaveContent.appendChild(autoHookWrap); - workflowSaveContent.appendChild(saveWorkflowBtn); - workflowSaveWrap.appendChild(workflowSaveContent); - - const workflowUtilsWrap = detailsSection("Workflow Utilities"); - workflowUtilsWrap.open = true; - workflowUtilsWrap.style.padding = "8px 10px"; - workflowUtilsWrap.style.borderRadius = "8px"; - workflowUtilsWrap.style.border = "1px solid rgba(255,255,255,0.12)"; - workflowUtilsWrap.style.background = "rgba(15, 17, 22, 0.75)"; - workflowUtilsWrap.style.backdropFilter = "blur(6px)"; - - const workflowUtilsContent = document.createElement("div"); - workflowUtilsContent.style.display = "flex"; - workflowUtilsContent.style.flexDirection = "column"; - workflowUtilsContent.style.gap = UI_GAP; - - const fixMissingBtn = btn("Fix Missing Models"); - fixMissingBtn.style.alignSelf = "flex-start"; - - const downloadMissingBtn = btn("Download Missing Models"); - downloadMissingBtn.style.alignSelf = "flex-start"; - - const utilsButtonRow = document.createElement("div"); - utilsButtonRow.style.display = "flex"; - utilsButtonRow.style.gap = UI_GAP; - utilsButtonRow.style.justifyContent = "flex-start"; - utilsButtonRow.style.alignItems = "center"; - utilsButtonRow.style.flexWrap = "wrap"; - utilsButtonRow.appendChild(fixMissingBtn); - utilsButtonRow.appendChild(downloadMissingBtn); - - const fixerStatus = document.createElement("div"); - fixerStatus.style.opacity = "0.85"; - fixerStatus.style.fontSize = "9px"; - fixerStatus.style.color = "#f2d98a"; - fixerStatus.style.lineHeight = "1.35"; - fixerStatus.style.marginLeft = "2px"; - - const downloadStatus = document.createElement("div"); - downloadStatus.style.display = "flex"; - downloadStatus.style.flexDirection = "column"; - downloadStatus.style.gap = "4px"; - downloadStatus.style.opacity = "0.85"; - downloadStatus.style.fontSize = "9px"; - downloadStatus.style.color = "#f2d98a"; - downloadStatus.style.lineHeight = "1.35"; - downloadStatus.style.marginLeft = "2px"; - - const downloadStatusText = document.createElement("div"); - downloadStatus.appendChild(downloadStatusText); - - const downloadProgressWrap = document.createElement("div"); - downloadProgressWrap.style.height = "6px"; - downloadProgressWrap.style.width = "100%"; - downloadProgressWrap.style.borderRadius = "999px"; - downloadProgressWrap.style.border = "1px solid rgba(255,255,255,0.12)"; - downloadProgressWrap.style.background = "rgba(0,0,0,0.25)"; - downloadProgressWrap.style.overflow = "hidden"; - - const downloadProgressBar = document.createElement("div"); - downloadProgressBar.style.height = "100%"; - downloadProgressBar.style.width = "0%"; - downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; - downloadProgressBar.style.transition = "width 0.2s ease"; - downloadProgressWrap.appendChild(downloadProgressBar); - downloadStatus.appendChild(downloadProgressWrap); - - const fingerprintDetails = detailsSection("Advanced (Fingerprint)"); - fingerprintDetails.style.marginTop = "0"; - - const fingerprintContent = document.createElement("div"); - fingerprintContent.style.display = "flex"; - fingerprintContent.style.flexDirection = "column"; - fingerprintContent.style.gap = UI_GAP; - - const buildFingerprintBtn = btn("Build Model Fingerprint Cache"); - buildFingerprintBtn.style.alignSelf = "flex-start"; - - fingerprintContent.appendChild(buildFingerprintBtn); - fingerprintDetails.appendChild(fingerprintContent); - - workflowUtilsContent.appendChild(utilsButtonRow); - workflowUtilsContent.appendChild(fixerStatus); - workflowUtilsContent.appendChild(downloadStatus); - workflowUtilsContent.appendChild(fingerprintDetails); - workflowUtilsWrap.appendChild(workflowUtilsContent); - - container.appendChild(statusBar); - container.appendChild(workflowWrap); - container.appendChild(resolveWrap); - container.appendChild(divider()); - container.appendChild(modelSection); - - const registrySection = detailsSection("Community Registry"); - registrySection.style.display = "flex"; - registrySection.style.flexDirection = "column"; - registrySection.style.gap = "10px"; - const registryInput = document.createElement("input"); - registryInput.placeholder = "search model name or alias"; - registryInput.style.width = "100%"; - registryInput.style.padding = "6px 8px"; - registryInput.style.borderRadius = "8px"; - registryInput.style.border = "1px solid rgba(255,255,255,0.15)"; - registryInput.style.background = "rgba(255,255,255,0.04)"; - registryInput.style.color = "#f4f6fb"; - registryInput.style.fontSize = "13px"; - - const registryActions = document.createElement("div"); - registryActions.style.display = "flex"; - registryActions.style.gap = "8px"; - registryActions.style.flexWrap = "wrap"; - - const registrySearchBtn = btn("Search registry"); - const registryContributeBtn = btn("Contribute URL"); - registryActions.appendChild(registrySearchBtn); - registryActions.appendChild(registryContributeBtn); - - const registryStatus = document.createElement("div"); - registryStatus.style.fontSize = "12px"; - registryStatus.style.display = "block"; - registryStatus.style.minHeight = "18px"; - registryStatus.style.opacity = "0.8"; - - const registryResultsList = document.createElement("div"); - registryResultsList.style.display = "flex"; - registryResultsList.style.flexDirection = "column"; - registryResultsList.style.gap = "10px"; - - const renderRegistryResults = (results) => { - registryResultsList.innerHTML = ""; - if (!results || results.length === 0) { - registryResultsList.textContent = "No registry matches yet."; - return; - } - for (const item of results) { - const card = document.createElement("div"); - card.style.border = "1px solid rgba(255,255,255,0.15)"; - card.style.borderRadius = "10px"; - card.style.padding = "10px"; - card.style.display = "flex"; - card.style.flexDirection = "column"; - card.style.gap = "4px"; - card.style.background = "rgba(255,255,255,0.02)"; - - const titleRow = document.createElement("div"); - titleRow.style.display = "flex"; - titleRow.style.justifyContent = "space-between"; - titleRow.style.alignItems = "baseline"; - - const title = document.createElement("strong"); - title.textContent = item.name || "Unnamed model"; - title.style.fontSize = "14px"; - title.style.wordBreak = "break-word"; - - const score = document.createElement("span"); - score.textContent = `${Math.round(item.match_score || 0)}%`; - score.style.color = "#a9ff8f"; - score.style.fontSize = "12px"; - titleRow.appendChild(title); - titleRow.appendChild(score); - - const meta = document.createElement("div"); - meta.style.fontSize = "11px"; - meta.style.opacity = "0.8"; - meta.textContent = `${item.platform || "community"} · ${item.type || "checkpoint"}`; - - const footer = document.createElement("div"); - footer.style.display = "flex"; - footer.style.gap = "8px"; - - if (item.url) { - const openBtn = document.createElement("button"); - openBtn.textContent = "Open"; - openBtn.className = "mjr-ps-btn"; - openBtn.style.flex = "1"; - openBtn.addEventListener("click", () => window.open(item.url, "_blank")); - footer.appendChild(openBtn); - } - - const copyBtn = document.createElement("button"); - copyBtn.textContent = "Copy URL"; - copyBtn.className = "mjr-ps-btn"; - copyBtn.style.flex = "1"; - copyBtn.addEventListener("click", async () => { - if (item.url) { - await navigator.clipboard.writeText(item.url); - toast("success", "Copied", "URL added to clipboard."); + const templateInput = document.createElement("input"); + templateInput.type = "text"; + templateInput.placeholder = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + templateInput.value = + state.pathTemplate || "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + const templateTokens = document.createElement("div"); + templateTokens.style.opacity = "0.85"; + templateTokens.style.fontSize = "8px"; + templateTokens.style.color = "#f2d98a"; + templateTokens.style.textAlign = "left"; + templateTokens.style.lineHeight = "1.35"; + templateTokens.style.wordBreak = "break-word"; + templateTokens.textContent = `Tokens: ${TEMPLATE_TOKENS.join(" ")}`; + const previewLabel = document.createElement("div"); + previewLabel.style.opacity = "0.85"; + previewLabel.style.fontSize = "8px"; + previewLabel.style.color = "#f2d98a"; + previewLabel.style.textAlign = "left"; + previewLabel.style.lineHeight = "1.35"; + previewLabel.style.wordBreak = "break-word"; + previewLabel.textContent = "Preview: -"; + advanced.appendChild(summary); + advanced.appendChild(row("Path Template", templateInput)); + advanced.appendChild(templateTokens); + advanced.appendChild(previewLabel); + const workflowSaveWrap = detailsSection("Workflow"); + workflowSaveWrap.open = true; + workflowSaveWrap.style.padding = "8px 10px"; + workflowSaveWrap.style.borderRadius = "8px"; + workflowSaveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowSaveWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowSaveWrap.style.backdropFilter = "blur(6px)"; + const workflowSaveContent = document.createElement("div"); + workflowSaveContent.style.display = "flex"; + workflowSaveContent.style.flexDirection = "column"; + workflowSaveContent.style.gap = UI_GAP; + workflowSaveContent.appendChild(row("Workflow name", workflowNameInput)); + workflowSaveContent.appendChild(row("Asset folder", workflowAssetInput)); + workflowSaveContent.appendChild(workflowPreviewLabel); + workflowSaveContent.appendChild(autoHookWrap); + workflowSaveContent.appendChild(saveWorkflowBtn); + workflowSaveWrap.appendChild(workflowSaveContent); + const workflowUtilsWrap = detailsSection("Workflow Utilities"); + workflowUtilsWrap.open = true; + workflowUtilsWrap.style.padding = "8px 10px"; + workflowUtilsWrap.style.borderRadius = "8px"; + workflowUtilsWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowUtilsWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowUtilsWrap.style.backdropFilter = "blur(6px)"; + const workflowUtilsContent = document.createElement("div"); + workflowUtilsContent.style.display = "flex"; + workflowUtilsContent.style.flexDirection = "column"; + workflowUtilsContent.style.gap = UI_GAP; + const fixMissingBtn = btn("Fix Missing Models"); + fixMissingBtn.style.alignSelf = "flex-start"; + const downloadMissingBtn = btn("Download Missing Models"); + downloadMissingBtn.style.alignSelf = "flex-start"; + const utilsButtonRow = document.createElement("div"); + utilsButtonRow.style.display = "flex"; + utilsButtonRow.style.gap = UI_GAP; + utilsButtonRow.style.justifyContent = "flex-start"; + utilsButtonRow.style.alignItems = "center"; + utilsButtonRow.style.flexWrap = "wrap"; + utilsButtonRow.appendChild(fixMissingBtn); + utilsButtonRow.appendChild(downloadMissingBtn); + const fixerStatus = document.createElement("div"); + fixerStatus.style.opacity = "0.85"; + fixerStatus.style.fontSize = "9px"; + fixerStatus.style.color = "#f2d98a"; + fixerStatus.style.lineHeight = "1.35"; + fixerStatus.style.marginLeft = "2px"; + const downloadStatus = document.createElement("div"); + downloadStatus.style.display = "flex"; + downloadStatus.style.flexDirection = "column"; + downloadStatus.style.gap = "4px"; + downloadStatus.style.opacity = "0.85"; + downloadStatus.style.fontSize = "9px"; + downloadStatus.style.color = "#f2d98a"; + downloadStatus.style.lineHeight = "1.35"; + downloadStatus.style.marginLeft = "2px"; + const downloadStatusText = document.createElement("div"); + downloadStatus.appendChild(downloadStatusText); + const downloadProgressWrap = document.createElement("div"); + downloadProgressWrap.style.height = "6px"; + downloadProgressWrap.style.width = "100%"; + downloadProgressWrap.style.borderRadius = "999px"; + downloadProgressWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + downloadProgressWrap.style.background = "rgba(0,0,0,0.25)"; + downloadProgressWrap.style.overflow = "hidden"; + const downloadProgressBar = document.createElement("div"); + downloadProgressBar.style.height = "100%"; + downloadProgressBar.style.width = "0%"; + downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; + downloadProgressBar.style.transition = "width 0.2s ease"; + downloadProgressWrap.appendChild(downloadProgressBar); + downloadStatus.appendChild(downloadProgressWrap); + const fingerprintDetails = detailsSection("Advanced (Fingerprint)"); + fingerprintDetails.style.marginTop = "0"; + const fingerprintContent = document.createElement("div"); + fingerprintContent.style.display = "flex"; + fingerprintContent.style.flexDirection = "column"; + fingerprintContent.style.gap = UI_GAP; + const buildFingerprintBtn = btn("Build Model Fingerprint Cache"); + buildFingerprintBtn.style.alignSelf = "flex-start"; + fingerprintContent.appendChild(buildFingerprintBtn); + fingerprintDetails.appendChild(fingerprintContent); + workflowUtilsContent.appendChild(utilsButtonRow); + workflowUtilsContent.appendChild(fixerStatus); + workflowUtilsContent.appendChild(downloadStatus); + workflowUtilsContent.appendChild(fingerprintDetails); + workflowUtilsWrap.appendChild(workflowUtilsContent); + container.appendChild(statusBar); + container.appendChild(workflowWrap); + container.appendChild(resolveWrap); + container.appendChild(divider()); + container.appendChild(modelSection); + const registrySection = detailsSection("Community Registry"); + registrySection.style.display = "flex"; + registrySection.style.flexDirection = "column"; + registrySection.style.gap = "10px"; + const registryInput = document.createElement("input"); + registryInput.placeholder = "search model name or alias"; + registryInput.style.width = "100%"; + registryInput.style.padding = "6px 8px"; + registryInput.style.borderRadius = "8px"; + registryInput.style.border = "1px solid rgba(255,255,255,0.15)"; + registryInput.style.background = "rgba(255,255,255,0.04)"; + registryInput.style.color = "#f4f6fb"; + registryInput.style.fontSize = "13px"; + const registryActions = document.createElement("div"); + registryActions.style.display = "flex"; + registryActions.style.gap = "8px"; + registryActions.style.flexWrap = "wrap"; + const registrySearchBtn = btn("Search registry"); + const registryContributeBtn = btn("Contribute URL"); + registryActions.appendChild(registrySearchBtn); + registryActions.appendChild(registryContributeBtn); + const registryStatus = document.createElement("div"); + registryStatus.style.fontSize = "12px"; + registryStatus.style.display = "block"; + registryStatus.style.minHeight = "18px"; + registryStatus.style.opacity = "0.8"; + const registryResultsList = document.createElement("div"); + registryResultsList.style.display = "flex"; + registryResultsList.style.flexDirection = "column"; + registryResultsList.style.gap = "10px"; + const renderRegistryResults = (results) => { + registryResultsList.innerHTML = ""; + if (!results || results.length === 0) { + registryResultsList.textContent = "No registry matches yet."; + return; } - }); - footer.appendChild(copyBtn); - - card.appendChild(titleRow); - card.appendChild(meta); - card.appendChild(footer); - registryResultsList.appendChild(card); - } - }; - - renderRegistryResults([]); - - const triggerRegistrySearch = async () => { - const query = registryInput.value.trim(); - if (!query) { - registryStatus.textContent = "Enter a query to search the registry."; - return; - } - registryStatus.textContent = "Searching registry..."; - registrySearchBtn.disabled = true; - try { - const resp = await searchRegistry(query, 6); - const results = resp?.results || []; - registryStatus.textContent = `Found ${results.length} result${results.length !== 1 ? "s" : ""}.`; - renderRegistryResults(results); - } catch (error) { - console.error("[registry search]", error); - registryStatus.textContent = "Registry search failed."; - } finally { - registrySearchBtn.disabled = false; - } - }; - - registrySearchBtn.addEventListener("click", triggerRegistrySearch); - registryInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - triggerRegistrySearch(); - } - }); - registryContributeBtn.addEventListener("click", () => showModelContributionDialog()); - - registrySection.appendChild(registryInput); - registrySection.appendChild(registryActions); - registrySection.appendChild(registryStatus); - registrySection.appendChild(registryResultsList); - container.appendChild(registrySection); - - container.appendChild(autoPatchNewWrap); - container.appendChild(autoSwitchWrap); - container.appendChild(divider()); - container.appendChild(workflowSaveWrap); - container.appendChild(workflowUtilsWrap); - container.appendChild(divider()); - customOutputSection.appendChild(row("Kind", kindSelect)); - customOutputSection.appendChild(row("Media", mediaSelect)); - customOutputSection.appendChild(groupRow("Name", nameStack)); - customOutputSection.appendChild(autoPatchWrap); - customOutputSection.appendChild(buttonRow); - customOutputSection.appendChild(targetLabel); - container.appendChild(customOutputSection); - container.appendChild(divider()); - container.appendChild(advanced); - - el.appendChild(container); - updateStatus(); - updateResolve(); - updateWorkflowBlock(); - - const fixerState = { - missing: 0, - fixed: 0, - total: 0, - cacheCount: 0, - cacheUpdatedAt: "", - cacheError: "", - scanned: false, - lastScanToken: -1, - }; - - const downloadState = { - state: "idle", - message: "", - progress: { current: 0, total: 0, pct: 0 }, - }; - - const updateFixerStatus = () => { - const cacheText = fixerState.cacheError - ? "error" - : `${fixerState.cacheCount} / ${fixerState.cacheUpdatedAt || "-"}`; - const missingText = fixerState.scanned ? fixerState.missing : "-"; - const fixedText = fixerState.scanned ? fixerState.fixed : "-"; - fixerStatus.textContent = `Missing: ${missingText} | Fixed: ${fixedText} | Cache: ${cacheText}`; - - const total = fixerState.total || 0; - let color = "#666666"; - if (fixerState.scanned) { - if (total > 0 && fixerState.missing === 0) { - color = "#2f8f4e"; - } else if (total > 0 && fixerState.missing > 0 && fixerState.missing < total) { - color = "#c77c2a"; - } else if (total > 0 && fixerState.missing >= total) { - color = "#b33a3a"; - } - } - workflowUtilsWrap.style.border = `1px solid ${colorWithAlpha(color, 0.4)}`; - workflowUtilsWrap.style.background = colorWithAlpha(color, 0.12); - }; - - const updateDownloadStatus = () => { - const rawState = downloadState.state || "idle"; - const stateLabel = rawState === "queued" ? "downloading" : rawState; - const pct = downloadState.progress?.pct || 0; - const pctText = pct ? ` ${pct}%` : ""; - const msg = downloadState.message ? ` | ${downloadState.message}` : ""; - downloadStatusText.textContent = `Download status: ${stateLabel}${pctText}${msg}`; - - let barPct = Math.max(0, Math.min(100, Number(pct) || 0)); - if (rawState === "done" && barPct === 0) { - barPct = 100; - } - if (rawState === "downloading" && barPct === 0 && downloadState.progress?.current) { - barPct = 5; - } - downloadProgressBar.style.width = `${barPct}%`; - - if (rawState === "error") { - downloadProgressBar.style.background = "rgba(179, 58, 58, 0.7)"; - } else if (rawState === "done") { - downloadProgressBar.style.background = "rgba(47, 143, 78, 0.7)"; - } else if (rawState === "idle") { - downloadProgressBar.style.background = "rgba(255, 255, 255, 0.2)"; - } else { - downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; - } - }; - - const refreshFingerprintStatus = async () => { - try { - const resp = await getFingerprintCacheStatus(); - fixerState.cacheCount = Number(resp?.count || 0); - fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); - fixerState.cacheError = ""; - } catch (e) { - fixerState.cacheError = "error"; - } - updateFixerStatus(); - }; - - updateFixerStatus(); - updateDownloadStatus(); - - let missingScanTimer = null; - const refreshMissingStatus = (force = false) => { - const autoScan = getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true); - if (!force && !autoScan) { - updateFixerStatus(); - return; - } - - const scanToken = Number(state.graphLoadToken || 0); - if (!force && fixerState.scanned && fixerState.lastScanToken === scanToken) { - updateFixerStatus(); - return; - } - - if (missingScanTimer) { - clearTimeout(missingScanTimer); - missingScanTimer = null; - } - - missingScanTimer = setTimeout(() => { - try { - const stats = scanMissingModelsWithStats(); - fixerState.missing = stats.missing.length; - fixerState.total = stats.total || 0; - fixerState.scanned = true; - fixerState.lastScanToken = scanToken; - if (fixerState.missing === 0) { - fixerState.fixed = 0; - } - } finally { - updateFixerStatus(); - } - }, 0); - }; - - const isEditableTarget = (target) => { - if (!target) return false; - const tag = String(target.tagName || "").toLowerCase(); - return target.isContentEditable || tag === "input" || tag === "textarea" || tag === "select"; - }; - - container.addEventListener("keydown", (event) => { - const key = String(event.key || "").toLowerCase(); - if (event.altKey && !event.ctrlKey && !event.metaKey) { - if (key === "a") { - event.preventDefault(); - createApplyBtn.click(); - } else if (key === "p") { - event.preventDefault(); - patchNowBtn.click(); - } - } - }); - - const resolveModel = () => { - if (useCustomCb.checked) { - const tag = token3Tag(customModelInput.value, true); - return { - modelRaw: customModelInput.value || "Unknown", - modelUpper: true, - tag: tag || "UNKNOWN", - }; - } - if (state.detectedModelTag) { - return { - modelRaw: state.detectedModelTag, - modelUpper: false, - tag: state.detectedModelTag, - }; - } - const sel = modelSelect.value || "Unknown"; - const tag = token3Tag(sel, false); - return { modelRaw: sel, modelUpper: false, tag: tag || "Unknown" }; - }; - - const buildWorkflowName = () => { - const modelInfo = resolveModel(); - const modelTag = modelInfo.tag || "Model"; - const assetName = titlePathJS(nameInput.value || "Asset"); - return `${yymmddJS()}_${modelTag}_${assetName}`; - }; - - const buildWorkflowPreview = () => { - const base = buildWorkflowName(); - const next = - base && state.workflowLastBase === base - ? Number(state.workflowNextSuffix || 1) - : 1; - const suffix = String(next).padStart(4, "0"); - return `${base}_${suffix}.json`; - }; - - let existingNamesToken = 0; - let lastExistingKey = ""; - - const refreshExistingNames = async (force = false) => { - const projectId = state.projectId || ""; - const media = mediaSelect.value || "images"; - const key = `${projectId}::${media}`; - if (!force && key === lastExistingKey) return; - lastExistingKey = key; - - existingNamesSelect.innerHTML = ""; - const emptyOpt = document.createElement("option"); - emptyOpt.value = ""; - emptyOpt.textContent = "-- Select existing name --"; - existingNamesSelect.appendChild(emptyOpt); - - if (!projectId) { - existingNamesSelect.style.display = "none"; - return; - } - - const requestToken = (existingNamesToken += 1); - try { - const names = await listExistingNames(projectId, media); - if (requestToken !== existingNamesToken) return; - if (!names.length) { - existingNamesSelect.style.display = "none"; - return; - } - for (const name of names) { - const opt = document.createElement("option"); - opt.value = name; - opt.textContent = name; - existingNamesSelect.appendChild(opt); - } - if (nameInput.value && names.includes(nameInput.value)) { - existingNamesSelect.value = nameInput.value; - } else { - existingNamesSelect.value = ""; - } - existingNamesSelect.style.display = "block"; - } catch (_) { - if (requestToken !== existingNamesToken) return; - existingNamesSelect.style.display = "none"; - } - }; - - let previewToken = 0; - - const updatePreview = async () => { - const baseRel = state.projectFolder - ? `PROJECTS/${state.projectFolder}` - : "PROJECTS/"; - const modelInfo = resolveModel(); - const tokens = { - BASE: baseRel, - MEDIA: mediaDir(mediaSelect.value), - DATE: yymmddJS(), - MODEL: String(modelInfo.tag || ""), - NAME: titlePathJS(nameInput.value || "Name"), - KIND: makeKindToken(kindSelect.value), - }; - const template = String(templateInput.value || "").trim(); - const requestToken = (previewToken += 1); - - detectedModelLabel.textContent = `Detected: ${state.detectedModelTag || "-"}`; - customModelPreview.textContent = useCustomCb.checked - ? `Custom tag: ${modelInfo.tag}` - : "Custom tag: -"; - if (modelSummary) { - const activeTag = modelInfo.tag || "-"; - modelSummary.textContent = `MODEL (${activeTag})`; - } - const assetFolder = titlePathJS(workflowAssetInput.value || ""); - const workflowFile = buildWorkflowPreview(); - const workflowRel = assetFolder - ? `03_WORKFLOWS/${assetFolder}/${workflowFile}` - : `03_WORKFLOWS/${workflowFile}`; - workflowPreviewLabel.textContent = `Next save: ${workflowRel}`; - modelSelect.disabled = !!state.detectedModelTag && !useCustomCb.checked; - modelSelect.style.opacity = modelSelect.disabled ? "0.6" : "1"; - - if (!template) { - previewLabel.textContent = "Preview: -"; - return; - } - - previewLabel.textContent = "Preview: ..."; - try { - const resp = await previewTemplate(template, tokens); - if (requestToken !== previewToken) return; - const preview = resp?.preview || ""; - previewLabel.textContent = preview ? `Preview: ${preview}` : "Preview: -"; - } catch (err) { - if (requestToken !== previewToken) return; - const previewError = String(err?.message || err || "Invalid template"); - previewLabel.textContent = `Preview: ${previewError}`; - } - }; - - const updateState = () => { - state.workflowName = workflowNameInput.value; - state.workflowAsset = workflowAssetInput.value; - state.kind = kindSelect.value; - state.media = mediaSelect.value; - state.name = nameInput.value; - state.customModelText = customModelInput.value; - state.modelSelection = modelSelect.value; - state.autoPatch = autoPatchCb.checked; - state.autoPatchNewNodes = autoPatchNewCb.checked; - state.autoSwitchTrusted = autoSwitchCb.checked; - state.autoHookSave = autoHookCb.checked; - state.useCustomModel = useCustomCb.checked; - state.modelUpper = useCustomCb.checked; - state.pendingProjectName = pendingProjectNameInput.value; - state.pendingSelectProjectId = pendingSelect.value; - state.pathTemplate = templateInput.value; - customModelWrap.style.display = useCustomCb.checked ? "block" : "none"; - saveState(state); - updateStatus(); - updateResolve(); - updateWorkflowBlock(); - updatePreview(); - if (!state.lastRelDir) { - targetLabel.textContent = "Target: none (create an output)"; - } - }; - - const setWorkflowName = (value) => { - workflowNameInput.value = value || ""; - state.workflowName = workflowNameInput.value; - saveState(state); - updatePreview(); - }; - - const resetForProjectChange = () => { - state.projectName = ""; - state.workflowName = ""; - state.workflowAsset = ""; - state.workflowLastBase = ""; - state.workflowNextSuffix = 1; - state.workflowPanelOpen = false; - state.name = ""; - state.kind = "asset"; - state.media = "images"; - state.modelSelection = ""; - state.useCustomModel = false; - state.customModelText = ""; - state.modelUpper = false; - state.lastRelDir = ""; - state.lastPrefix = ""; - state.pathTemplate = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; - - workflowNameInput.value = ""; - workflowAssetInput.value = ""; - nameInput.value = ""; - kindSelect.value = "asset"; - mediaSelect.value = "images"; - if (modelSelect.options.length > 0) { - modelSelect.selectedIndex = 0; - } - useCustomCb.checked = false; - customModelInput.value = ""; - customModelWrap.style.display = "none"; - templateInput.value = state.pathTemplate; - targetLabel.textContent = "Target: none (create an output)"; - updateState(); - updatePreview(); - }; - - // Assign UI helper functions after all are defined - state._ui = { - updateStatus, - updateResolve, - updatePreview, - updateWorkflowBlock, - updateTargetLabel, - resetForProjectChange, - buildWorkflowName, - setWorkflowName, - refreshExistingNames, - refreshProjectsList: (force = false) => refreshProjectsList(force), - refreshMissingStatus, - }; - - // Store full project list for search filtering - let allProjects = []; - let lastProjectsRefreshAt = 0; - let projectRefreshErrorCount = 0; - - const updateProjectDropdowns = (filteredList) => { - pendingSelect.innerHTML = ""; - const pendingEmpty = document.createElement("option"); - pendingEmpty.value = ""; - pendingEmpty.textContent = "-"; - pendingSelect.appendChild(pendingEmpty); - - for (const p of filteredList) { - const opt = document.createElement("option"); - opt.value = p.project_id; - opt.dataset.folder = p.folder || ""; - opt.dataset.exists = p.exists === false ? "false" : "true"; - const missingTag = p.exists === false ? " (missing)" : ""; - const archivedTag = p.archived ? " [archived]" : ""; - opt.textContent = `${p.folder || p.project_id} (${p.project_id})${missingTag}${archivedTag}`; - pendingSelect.appendChild(opt); - } - }; - - refreshProjectsList = async (force = false) => { - const now = Date.now(); - - // Apply exponential backoff on errors - const throttleMs = projectRefreshErrorCount > 0 - ? PROJECT_REFRESH_ERROR_BACKOFF_MS * Math.min(projectRefreshErrorCount, 5) - : PROJECT_REFRESH_THROTTLE_MS; - - if (!force && now - lastProjectsRefreshAt < throttleMs) return; - - lastProjectsRefreshAt = now; - try { - const resp = await fetchJSON("/mjr_project/list"); - allProjects = resp.projects || []; - state.lastError = ""; - projectRefreshErrorCount = 0; // Reset error count on success - - updateProjectDropdowns(allProjects); - if (state.projectId) { - const active = allProjects.find((p) => p.project_id === state.projectId); - state.projectExists = active ? active.exists !== false : false; - } - if (state.pendingSelectProjectId) { - pendingSelect.value = state.pendingSelectProjectId; - } - updateStatus(); - updateResolve(); - updateWorkflowBlock(); - } catch (e) { - projectRefreshErrorCount++; - const errorMsg = String(e.message || e); - state.lastError = errorMsg; - updateStatus(); - - // Only show toast on first error to avoid spam - if (projectRefreshErrorCount === 1) { - toast("error", "Projects list failed", errorMsg); - } - console.error("[mjr] Project list refresh failed:", e); - } - }; - - workflowNameInput.addEventListener("input", updateState); - workflowAssetInput.addEventListener("input", updateState); - kindSelect.addEventListener("change", updateState); - mediaSelect.addEventListener("change", () => { - updateState(); - refreshExistingNames(true); - }); - nameInput.addEventListener("input", updateState); - existingNamesSelect.addEventListener("change", () => { - nameInput.value = existingNamesSelect.value || ""; - updateState(); - }); - customModelInput.addEventListener("input", updateState); - useCustomCb.addEventListener("change", updateState); - autoPatchCb.addEventListener("change", updateState); - autoPatchNewCb.addEventListener("change", updateState); - autoSwitchCb.addEventListener("change", updateState); - autoHookCb.addEventListener("change", updateState); - templateInput.addEventListener("input", updateState); - pendingProjectNameInput.addEventListener("input", updateState); - pendingSelect.addEventListener("change", updateState); - - const runFixMissingModels = async () => { - debug("[MJR] Fix Missing Models clicked"); - const originalLabel = fixMissingBtn.textContent; - fixMissingBtn.disabled = true; - fixMissingBtn.textContent = "Scanning..."; - try { - debug("[MJR] Scanning for missing models..."); - const stats = scanMissingModelsWithStats(); - debug("[MJR] Scan results:", stats); - const missing = stats.missing; - fixerState.missing = missing.length; - fixerState.total = stats.total || 0; - fixerState.scanned = true; - fixerState.lastScanToken = Number(state.graphLoadToken || 0); - fixerState.fixed = 0; - updateFixerStatus(); - - if (!missing.length) { - debug("[MJR] No missing models found"); - toast("info", "No missing models", "All model widgets match available options."); - return; - } - - const payload = missing.map((item) => ({ - missing_value: item.missing_value, - type_hint: item.type_hint, - })); - debug("[MJR] Sending scan request to API:", payload); - const resp = await scanModelCandidates(payload); - debug("[MJR] API response:", resp); - const results = Array.isArray(resp?.results) ? resp.results : []; - const resultMap = new Map(); - for (const entry of results) { - const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; - resultMap.set(key, Array.isArray(entry?.candidates) ? entry.candidates : []); - } - - const autoFixes = []; - const autoFixedKeys = new Set(); - for (const entry of missing) { - const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; - const candidates = resultMap.get(key) || []; - const exact = candidates.filter( - (c) => Number(c?.score || 0) === 100 && !c?.in_wrong_folder && c?.reason !== "wrong_folder" - ); - if (exact.length === 1) { - const relpath = String(exact[0]?.relpath || ""); - if (relpath) { - autoFixes.push({ - node_id: entry?.node_id, - widget_name: entry?.widget_name, - new_value: relpath, + for (const item of results) { + const card = document.createElement("div"); + card.style.border = "1px solid rgba(255,255,255,0.15)"; + card.style.borderRadius = "10px"; + card.style.padding = "10px"; + card.style.display = "flex"; + card.style.flexDirection = "column"; + card.style.gap = "4px"; + card.style.background = "rgba(255,255,255,0.02)"; + const titleRow = document.createElement("div"); + titleRow.style.display = "flex"; + titleRow.style.justifyContent = "space-between"; + titleRow.style.alignItems = "baseline"; + const title = document.createElement("strong"); + title.textContent = item.name || "Unnamed model"; + title.style.fontSize = "14px"; + title.style.wordBreak = "break-word"; + const score = document.createElement("span"); + score.textContent = `${Math.round(item.match_score || 0)}%`; + score.style.color = "#a9ff8f"; + score.style.fontSize = "12px"; + titleRow.appendChild(title); + titleRow.appendChild(score); + const meta = document.createElement("div"); + meta.style.fontSize = "11px"; + meta.style.opacity = "0.8"; + meta.textContent = `${item.platform || "community"} · ${item.type || "checkpoint"}`; + const footer = document.createElement("div"); + footer.style.display = "flex"; + footer.style.gap = "8px"; + if (item.url) { + const openBtn = document.createElement("button"); + openBtn.textContent = "Open"; + openBtn.className = "mjr-ps-btn"; + openBtn.style.flex = "1"; + openBtn.addEventListener("click", () => window.open(item.url, "_blank")); + footer.appendChild(openBtn); + } + const copyBtn = document.createElement("button"); + copyBtn.textContent = "Copy URL"; + copyBtn.className = "mjr-ps-btn"; + copyBtn.style.flex = "1"; + copyBtn.addEventListener("click", async () => { + if (item.url) { + await navigator.clipboard.writeText(item.url); + toast("success", "Copied", "URL added to clipboard."); + } }); - autoFixedKeys.add(`${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`); - } + footer.appendChild(copyBtn); + card.appendChild(titleRow); + card.appendChild(meta); + card.appendChild(footer); + registryResultsList.appendChild(card); } - } - - let fixedCount = 0; - let remaining = missing.filter((entry) => { - const key = `${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`; - return !autoFixedKeys.has(key); - }); - - if (autoFixes.length) { - fixedCount = applyFixesToGraph(autoFixes); - const postStats = scanMissingModelsWithStats(); - remaining = postStats.missing; - fixerState.missing = remaining.length; - fixerState.total = postStats.total || 0; - fixerState.scanned = true; - fixerState.lastScanToken = Number(state.graphLoadToken || 0); - fixerState.fixed = fixedCount; - updateFixerStatus(); - } - - if (!remaining.length) { - if (fixedCount > 0) { - toast("success", "Fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); - } else { - toast("info", "No missing models", "All model widgets match available options."); + }; + renderRegistryResults([]); + const triggerRegistrySearch = async () => { + const query = registryInput.value.trim(); + if (!query) { + registryStatus.textContent = "Enter a query to search the registry."; + return; } - return; - } - - const fixes = await showModelFixerDialog({ - missing: remaining, - results, - }); - debug("[MJR] User selected fixes:", fixes); - if (!fixes) { - if (fixedCount > 0) { - toast("success", "Auto-fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); - } else { - toast("info", "Fix cancelled", "No changes applied."); + registryStatus.textContent = "Searching registry..."; + registrySearchBtn.disabled = true; + try { + const resp = await searchRegistry(query, 6); + const results = resp?.results || []; + registryStatus.textContent = `Found ${results.length} result${results.length !== 1 ? "s" : ""}.`; + renderRegistryResults(results); } - return; - } - const fixed = applyFixesToGraph(fixes); - debug("[MJR] Applied fixes count:", fixed); - const postStats = scanMissingModelsWithStats(); - fixerState.missing = postStats.missing.length; - fixerState.total = postStats.total || 0; - fixerState.scanned = true; - fixerState.lastScanToken = Number(state.graphLoadToken || 0); - fixerState.fixed = fixedCount + fixed; - updateFixerStatus(); - if (fixed + fixedCount > 0) { - toast( - "success", - "Fix applied", - `Updated ${fixed + fixedCount} widget${fixed + fixedCount !== 1 ? "s" : ""}.` - ); - } else { - toast("warn", "No changes", "No widgets were updated."); - } - } catch (e) { - console.error("[MJR] Fix missing models error:", e); - toast("error", "Fix failed", String(e?.message || e)); - } finally { - fixMissingBtn.textContent = originalLabel; - fixMissingBtn.disabled = false; - } - }; - - const uniqueMissingForDownload = (missing) => { - const seen = new Set(); - const out = []; - for (const item of missing || []) { - const key = `${item.missing_value}::${item.type_hint || ""}`; - if (seen.has(key)) continue; - seen.add(key); - out.push(item); - } - return out; - }; - - const basename = (value) => normalizeKey(value); - - const pollDownloadJob = async (jobId) => { - while (true) { - try { - const status = await getDownloadStatus(jobId); - downloadState.state = status?.state || "downloading"; - downloadState.message = status?.message || ""; - downloadState.progress = status?.progress || { current: 0, total: 0, pct: 0 }; - updateDownloadStatus(); - if (downloadState.state === "done" || downloadState.state === "error") { - return status; + catch (error) { + console.error("[registry search]", error); + registryStatus.textContent = "Registry search failed."; } - } catch (e) { - downloadState.state = "error"; - downloadState.message = "status check failed"; - updateDownloadStatus(); - return null; - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - }; - - const runDownloadMissingModels = async () => { - debug("[MJR] Download Missing Models clicked"); - if (downloadState.state === "downloading") { - debug("[MJR] Download already in progress"); - toast("warn", "Download in progress", "Please wait for the current job to finish."); - return; - } - const originalLabel = downloadMissingBtn.textContent; - downloadMissingBtn.disabled = true; - downloadMissingBtn.textContent = "Scanning..."; - downloadState.state = "idle"; - downloadState.message = "scanning"; - updateDownloadStatus(); - - try { - debug("[MJR] Scanning for missing models..."); - const stats = scanMissingModelsWithStats(); - debug("[MJR] Scan results:", stats); - const missingAll = stats.missing; - const missing = uniqueMissingForDownload(missingAll); - debug("[MJR] Unique missing for download:", missing); - fixerState.missing = missingAll.length; - fixerState.total = stats.total || 0; - fixerState.scanned = true; - fixerState.lastScanToken = Number(state.graphLoadToken || 0); - updateFixerStatus(); - if (!missing.length) { - debug("[MJR] No missing models found"); - downloadState.state = "idle"; - downloadState.message = ""; - updateDownloadStatus(); - toast("info", "No missing models", "All model widgets match available options."); - return; - } - - const payload = missing.map((item) => ({ - missing_value: item.missing_value, - type_hint: item.type_hint, - })); - const workflowJson = getSerializedWorkflow(); - const noteTexts = extractNoteTexts(workflowJson); - const noteRecipes = collectNoteRecipes(workflowJson); - const resolved = await resolveModelRecipes(payload); - const resolvedMap = new Map(); - for (const entry of resolved?.resolved || []) { - const key = basename(entry.key || entry.missing_value); - resolvedMap.set(key, entry); - } - - const kindOptions = getKindOptions(); - let existingMap = null; - try { - const modelResp = await getModels(); - const categories = modelResp?.categories || {}; - existingMap = new Map(); - for (const [kind, items] of Object.entries(categories)) { - if (!Array.isArray(items)) continue; - const set = new Set(); - for (const item of items) { - const raw = String(item || "").replace(/\\/g, "/").toLowerCase(); - if (!raw) continue; - set.add(raw); - const base = raw.split("/").pop(); - if (base) set.add(base); - } - existingMap.set(kind, set); + finally { + registrySearchBtn.disabled = false; } - } catch (_) { - existingMap = null; - } - - // Helper to check if model exists - const modelExists = (kind, filename) => { - if (!existingMap || !kind || !filename) return false; - const set = existingMap.get(kind); - if (!set) return false; - const normalized = String(filename || "").replace(/\\/g, "/").toLowerCase(); - return set.has(normalized) || set.has(normalized.split("/").pop()); - }; - - let matches = 0; - const allEntries = missing.map((item) => { - const key = basename(item.missing_value); - const stored = resolvedMap.get(key) || {}; - const fallbackKind = stored.kind || typeHintToKind(item.type_hint); - const noteRecipe = noteRecipes.get(key); - let recipe = stored.recipe || null; - let prefilled = false; - if (noteRecipe) { - recipe = noteRecipe; - if (recipe?.kind === "unknown" && fallbackKind) { - recipe = { ...recipe, kind: fallbackKind }; - } - prefilled = true; - matches += 1; - } else if (recipe) { - recipe = { ...recipe, source: "stored" }; - prefilled = true; + }; + registrySearchBtn.addEventListener("click", triggerRegistrySearch); + registryInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + triggerRegistrySearch(); } - - const kind = stored.kind || fallbackKind; - const filename = recipe?.filename || key; - const exists = modelExists(kind, filename); - - return { - missing_value: item.missing_value, - key, - kind, - recipe, - prefilled, - exists, // Flag to track if model already downloaded - }; - }); - - // Filter to only show truly missing models (not already downloaded) - const dialogEntries = allEntries.filter((entry) => !entry.exists); - const alreadyDownloaded = allEntries.length - dialogEntries.length; - const entryMap = new Map(dialogEntries.map((entry) => [entry.key, entry])); - - console.debug("[mjr] download filter", { - total: allEntries.length, - missing: dialogEntries.length, - alreadyDownloaded, - notes: noteTexts.length, - recipes: noteRecipes.size, - matches, - }); - - if (dialogEntries.length === 0) { - downloadState.state = "idle"; - downloadState.message = ""; - updateDownloadStatus(); - if (alreadyDownloaded > 0) { - toast("success", "All models downloaded", `${alreadyDownloaded} model${alreadyDownloaded !== 1 ? 's' : ''} already in models folder.`); - } else { - toast("info", "No downloads needed", "No models to download."); - } - return; - } - - const dialogResult = await showModelDownloaderDialog({ - entries: dialogEntries, - kindOptions, - existingMap, - }); - if (!dialogResult) { - downloadState.state = "idle"; - downloadState.message = ""; - updateDownloadStatus(); - return; - } - - const items = dialogResult.items || []; - if (!items.length) { - downloadState.state = "idle"; - downloadState.message = ""; - updateDownloadStatus(); - toast("warn", "No downloads", "Select at least one entry to download."); - return; - } - - const autoRememberNotes = getSettingBool(SETTING_AUTO_REMEMBER_NOTE_SOURCES, true); - const saveItems = []; - if (dialogResult.remember && dialogResult.saveItems?.length) { - saveItems.push(...dialogResult.saveItems); - } - if (autoRememberNotes) { - for (const item of items) { - const entry = entryMap.get(item.key); - if (entry?.recipe?.source === "workflow_note") { - saveItems.push(item); - } - } - } - if (saveItems.length) { - const seenKeys = new Set(); - const deduped = []; - for (const item of saveItems) { - if (!item || !item.key) continue; - if (seenKeys.has(item.key)) continue; - seenKeys.add(item.key); - deduped.push(item); - } - if (deduped.length) { - await saveModelRecipes(deduped); - } - } - - const resp = await downloadModels(items); - const jobId = resp?.job_id; - if (!jobId) { - throw new Error("download job not created"); - } - downloadState.state = "downloading"; - downloadState.message = "queued"; - downloadState.progress = { current: 0, total: 0, pct: 0 }; - updateDownloadStatus(); - - downloadMissingBtn.textContent = "Downloading..."; - const finalStatus = await pollDownloadJob(jobId); - const summary = finalStatus?.summary || {}; - const downloaded = Number(summary.downloaded || 0); - const errors = Number(summary.errors || 0); - const skipped = Number(summary.skipped || 0); - if (downloadState.state === "error") { - toast( - "error", - "Download finished", - `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}` - ); - } else { - toast( - "success", - "Download finished", - `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}` - ); - } - - try { - loadModels(); - } catch (_) {} - try { - app.graph?.setDirtyCanvas?.(true, true); - } catch (_) {} - - const autoFix = getSettingBool(SETTING_AUTO_FIX_AFTER_DOWNLOAD, true); - if (autoFix) { - await runFixMissingModels(); - } else { - const runFix = await psConfirm({ - title: "Run Fix Missing Models?", - message: "Downloads finished. Run Fix Missing Models now?", - }); - if (runFix) { - await runFixMissingModels(); - } else { - toast("info", "Fix skipped", "Re-open model dropdowns if they do not refresh."); - } - } - } catch (e) { - downloadState.state = "error"; - downloadState.message = "download failed"; - updateDownloadStatus(); - toast("error", "Download failed", String(e?.message || e)); - } finally { - downloadMissingBtn.textContent = originalLabel; - downloadMissingBtn.disabled = false; - } - }; - - fixMissingBtn.addEventListener("click", runFixMissingModels); - downloadMissingBtn.addEventListener("click", runDownloadMissingModels); - - buildFingerprintBtn.addEventListener("click", async () => { - const originalLabel = buildFingerprintBtn.textContent; - buildFingerprintBtn.disabled = true; - buildFingerprintBtn.textContent = "Building..."; - try { - const resp = await buildFingerprintCache({}); - const count = Number(resp?.count || 0); - fixerState.cacheCount = count; - fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); - fixerState.cacheError = ""; - updateFixerStatus(); - const hashed = Number(resp?.hashed || 0); - const reused = Number(resp?.reused || 0); - toast("success", "Cache built", `Items: ${count} | Hashed: ${hashed} | Reused: ${reused}`); - } catch (e) { - toast("error", "Cache build failed", String(e?.message || e)); - } finally { - buildFingerprintBtn.textContent = originalLabel; - buildFingerprintBtn.disabled = false; - refreshFingerprintStatus(); - } - }); - - saveWorkflowBtn.addEventListener("click", async () => { - updateState(); - const wfName = workflowNameInput.value.trim(); - await saveWorkflowToProject(state, wfName); - }); - - switchBtn.addEventListener("click", async () => { - const det = state.detected; - if (!det || det.mode === "none") { - toast("warn", "No detected project", "Nothing to switch to."); - return; - } - try { - if (det.project_id) { - await setActiveProjectById(state, det.project_id); - } else if (det.project_folder) { - const resp = await fetchJSON("/mjr_project/set", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ project_name: det.project_folder, create_base: true }), - }); - state.projectId = resp.project_id; - state.projectFolder = resp.project_folder; - state.projectExists = true; - state.lastError = ""; - saveState(state); - resetForProjectChange(); - refreshProjectsList(true); - toast("success", "Project active", resp.project_folder); - } else { - toast("warn", "No detected project", "No folder available to switch."); - return; - } - state.detected = null; - updateResolve(); - state.workflowHasSignature = true; - updateWorkflowBlock(); - stampGraphProjectSignature(app, state); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Switch failed", state.lastError); - } - }); - - const assignWorkflowToActive = async () => { - if (!state.projectId || !state.projectFolder) { - toast("error", "No active project", "Set a project first."); - return; - } - stampGraphProjectSignature(app, state); - state.workflowHasSignature = true; - state.workflowPanelOpen = false; - toast("success", "Workflow assigned", state.projectFolder); - const doPatch = await psConfirm({ - title: "Patch save paths?", - message: "Patch save paths to the active project?", }); - if (doPatch) { - const base = `PROJECTS/${state.projectFolder}`; - const imgRel = joinRel(base, mediaDir("images")); - const vidRel = joinRel(base, mediaDir("videos")); - const patchedImages = await patchSaveNodes(app, imgRel, "", "images"); - const patchedVideos = await patchSaveNodes(app, vidRel, "", "videos"); - const patched = patchedImages + patchedVideos; - if (patched > 0) { - toast("success", "Patch applied", `Updated ${patched} nodes.`); - } else { - toast("warn", "No nodes patched", "No save-like nodes were updated."); - } - } else { - toast("info", "Patch skipped", "Save paths were not changed."); - } - state.detected = null; + registryContributeBtn.addEventListener("click", () => showModelContributionDialog()); + registrySection.appendChild(registryInput); + registrySection.appendChild(registryActions); + registrySection.appendChild(registryStatus); + registrySection.appendChild(registryResultsList); + container.appendChild(registrySection); + container.appendChild(autoPatchNewWrap); + container.appendChild(autoSwitchWrap); + container.appendChild(divider()); + container.appendChild(workflowSaveWrap); + container.appendChild(workflowUtilsWrap); + container.appendChild(divider()); + customOutputSection.appendChild(row("Kind", kindSelect)); + customOutputSection.appendChild(row("Media", mediaSelect)); + customOutputSection.appendChild(groupRow("Name", nameStack)); + customOutputSection.appendChild(autoPatchWrap); + customOutputSection.appendChild(buttonRow); + customOutputSection.appendChild(targetLabel); + container.appendChild(customOutputSection); + container.appendChild(divider()); + container.appendChild(advanced); + el.appendChild(container); updateStatus(); updateResolve(); updateWorkflowBlock(); - }; - - assignBtn.addEventListener("click", assignWorkflowToActive); - - createBtn.addEventListener("click", async () => { - const det = state.detected; - const suggested = det?.project_folder || "New_Project"; - const entered = await psPrompt({ - title: "Create project", - message: "Enter a project name", - defaultValue: suggested, - }); - if (entered === null) { - toast("info", "Create cancelled", "No project name provided."); - return; - } - const projectName = String(entered || "").trim(); - if (!projectName) { - toast("error", "Validation", "Project name is required."); - return; - } - try { - await createAndActivateProject(state, projectName, { - updateStatus, - updateResolve, - updateWorkflowBlock, - resetForProjectChange, - updatePreview, - refreshProjectsList, - }); - state.detected = null; - updateResolve(); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Create failed", state.lastError); - } - }); - - dismissBtn.addEventListener("click", () => { - state.detected = null; - updateResolve(); - }); - - wfCreateBtn.addEventListener("click", async () => { - const name = pendingProjectNameInput.value.trim(); - if (!name) { - toast("error", "Validation", "Project name is required."); - return; - } - try { - await createAndActivateProject(state, name, { - updateStatus, - updateResolve, - updateWorkflowBlock, - resetForProjectChange, - updatePreview, - refreshProjectsList, - }); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Create failed", state.lastError); - } - }); - - wfActivateBtn.addEventListener("click", async () => { - const projectId = pendingSelect.value; - if (!projectId) { - toast("error", "Validation", "Select a project first."); - return; - } - try { - await setActiveProjectById(state, projectId); - state.workflowHasSignature = true; - updateWorkflowBlock(); - stampGraphProjectSignature(app, state); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Activate failed", state.lastError); - } - }); - - wfAssignBtn.addEventListener("click", assignWorkflowToActive); - - modelSelect.addEventListener("change", updateState); - - const loadModels = async () => { - try { - const resp = await fetchJSON("/mjr_project/models"); - const categories = resp.categories || {}; - modelSelect.innerHTML = ""; - - const primaryCats = ["diffusion_models", "checkpoints"]; - const extraCats = Object.keys(categories).filter( - (c) => !primaryCats.includes(c) - ); - - const addGroup = (cat) => { - const items = categories[cat] || []; - if (!Array.isArray(items) || items.length === 0) return 0; - const group = document.createElement("optgroup"); - group.label = cat; - for (const item of items) { - const opt = document.createElement("option"); - opt.value = item; - opt.textContent = item; - group.appendChild(opt); + const fixerState = { + missing: 0, + fixed: 0, + total: 0, + cacheCount: 0, + cacheUpdatedAt: "", + cacheError: "", + scanned: false, + lastScanToken: -1, + }; + const downloadState = { + state: "idle", + message: "", + progress: { current: 0, total: 0, pct: 0 }, + }; + const updateFixerStatus = () => { + const cacheText = fixerState.cacheError + ? "error" + : `${fixerState.cacheCount} / ${fixerState.cacheUpdatedAt || "-"}`; + const missingText = fixerState.scanned ? fixerState.missing : "-"; + const fixedText = fixerState.scanned ? fixerState.fixed : "-"; + fixerStatus.textContent = `Missing: ${missingText} | Fixed: ${fixedText} | Cache: ${cacheText}`; + const total = fixerState.total || 0; + let color = "#666666"; + if (fixerState.scanned) { + if (total > 0 && fixerState.missing === 0) { + color = "#2f8f4e"; + } + else if (total > 0 && fixerState.missing > 0 && fixerState.missing < total) { + color = "#c77c2a"; + } + else if (total > 0 && fixerState.missing >= total) { + color = "#b33a3a"; + } } - modelSelect.appendChild(group); - return items.length; - }; - - let total = 0; - for (const cat of primaryCats) { - total += addGroup(cat); - } - if (state.showMoreModels) { - for (const cat of extraCats) { - total += addGroup(cat); + workflowUtilsWrap.style.border = `1px solid ${colorWithAlpha(color, 0.4)}`; + workflowUtilsWrap.style.background = colorWithAlpha(color, 0.12); + }; + const updateDownloadStatus = () => { + const rawState = downloadState.state || "idle"; + const stateLabel = rawState === "queued" ? "downloading" : rawState; + const pct = downloadState.progress?.pct || 0; + const pctText = pct ? ` ${pct}%` : ""; + const msg = downloadState.message ? ` | ${downloadState.message}` : ""; + downloadStatusText.textContent = `Download status: ${stateLabel}${pctText}${msg}`; + let barPct = Math.max(0, Math.min(100, Number(pct) || 0)); + if (rawState === "done" && barPct === 0) { + barPct = 100; } - } - - if (total === 0) { - const opt = document.createElement("option"); - opt.value = "Unknown"; - opt.textContent = "Unknown"; - modelSelect.appendChild(opt); - } - modelEmptyMsg.style.display = total === 0 ? "block" : "none"; - - if (state.modelSelection) { - modelSelect.value = state.modelSelection; - } - if (!modelSelect.value && modelSelect.options.length > 0) { - modelSelect.selectedIndex = 0; - } - updateState(); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Model list failed", state.lastError); - console.error(e); - } - }; - - modelToggleBtn.addEventListener("click", () => { - state.showMoreModels = !state.showMoreModels; - modelToggleBtn.textContent = state.showMoreModels ? "Show less" : "Show more"; - saveState(state); - loadModels(); - }); - - createApplyBtn.addEventListener("click", async () => { - if (!state.projectId) { - state.lastError = "set project first"; - updateStatus(); - toast("error", "Validation", "Set a project first."); - return; - } - const name = nameInput.value.trim(); - if (!name) { - state.lastError = "name required"; - updateStatus(); - toast("error", "Validation", "Name is required."); - return; - } - - if (useCustomCb.checked && !customModelInput.value.trim()) { - toast("error", "Validation", "Custom model is required."); - return; - } - const modelInfo = resolveModel(); - const modelName = modelInfo.modelRaw || "Unknown"; - - try { - const resp = await fetchJSON("/mjr_project/create_custom_out", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - project_id: state.projectId, - kind: kindSelect.value, - name, - media: mediaSelect.value, - model: modelName, - model_upper: modelInfo.modelUpper === true, - date: yymmddJS(), - template: templateInput.value, - }), - }); - - state.lastRelDir = resp.rel_dir; - state.lastPrefix = resp.filename_prefix; - state.media = resp.media || mediaSelect.value; - state.kind = resp.kind || kindSelect.value; - state.projectExists = true; - state.workflowHasSignature = true; - state.lastError = ""; - saveState(state); - updateStatus(); - updateWorkflowBlock(); - - targetLabel.textContent = `Target: ${resp.rel_dir} | Prefix: ${resp.filename_prefix}`; - - if (autoPatchCb.checked) { - const patched = await patchSaveNodes( - app, - resp.rel_dir, - resp.filename_prefix, - resp.media - ); - if (patched > 0) { - toast("success", "Patch applied", `Updated ${patched} nodes.`); - } else { - toast("warn", "No nodes patched", "No save-like nodes were updated."); + if (rawState === "downloading" && barPct === 0 && downloadState.progress?.current) { + barPct = 5; } - } - stampGraphProjectSignature(app, state); - toast("success", "Folder created", resp.rel_dir); - refreshProjectsList(true); - } catch (e) { - state.lastError = String(e.message || e); - updateStatus(); - toast("error", "Create failed", state.lastError); - console.error(e); - } - }); - - patchNowBtn.addEventListener("click", async () => { - rollOutputDateIfNeeded(); - if (!state.lastRelDir || !state.lastPrefix) { - targetLabel.textContent = "Target: none (create an output)"; - toast("warn", "No target", "Create an output first."); - return; - } - - // Count save nodes first - const saveNodes = (app.graph?._nodes || []).filter(n => isSaveLikeNode(n)); - const nodeCount = saveNodes.length; - - if (nodeCount === 0) { - toast("warn", "No nodes to patch", "No save-like nodes found in workflow."); - return; - } - - // Ask for confirmation - const confirmed = await psConfirm({ - title: "Patch save paths?", - message: - `Update ${nodeCount} save node${nodeCount !== 1 ? "s" : ""} to use the project path?\n\n` + - `Path: ${state.lastRelDir}\n` + - `Prefix: ${state.lastPrefix}\n\n` + - `This can be undone with Ctrl+Z.`, - }); - - if (!confirmed) { - toast("info", "Cancelled", "No nodes were modified."); - return; - } - - const patched = await patchSaveNodes(app, state.lastRelDir, state.lastPrefix, state.media); - if (patched > 0) { - toast("success", "Patch applied", `Updated ${patched} node${patched !== 1 ? 's' : ''}.`); - } else { - toast("warn", "No nodes patched", "No save-like nodes were updated."); - } - stampGraphProjectSignature(app, state); - }); - - loadModels(); - refreshProjectsList(true); - refreshExistingNames(true); - updatePreview(); - refreshFingerprintStatus(); - if (getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true)) { - refreshMissingStatus(true); - } else { + downloadProgressBar.style.width = `${barPct}%`; + if (rawState === "error") { + downloadProgressBar.style.background = "rgba(179, 58, 58, 0.7)"; + } + else if (rawState === "done") { + downloadProgressBar.style.background = "rgba(47, 143, 78, 0.7)"; + } + else if (rawState === "idle") { + downloadProgressBar.style.background = "rgba(255, 255, 255, 0.2)"; + } + else { + downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; + } + }; + const refreshFingerprintStatus = async () => { + try { + const resp = await getFingerprintCacheStatus(); + fixerState.cacheCount = Number(resp?.count || 0); + fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); + fixerState.cacheError = ""; + } + catch (e) { + fixerState.cacheError = "error"; + } + updateFixerStatus(); + }; updateFixerStatus(); - } + updateDownloadStatus(); + let missingScanTimer = null; + const refreshMissingStatus = (force = false) => { + const autoScan = getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true); + if (!force && !autoScan) { + updateFixerStatus(); + return; + } + const scanToken = Number(state.graphLoadToken || 0); + if (!force && fixerState.scanned && fixerState.lastScanToken === scanToken) { + updateFixerStatus(); + return; + } + if (missingScanTimer) { + clearTimeout(missingScanTimer); + missingScanTimer = null; + } + missingScanTimer = setTimeout(() => { + try { + const stats = scanMissingModelsWithStats(); + fixerState.missing = stats.missing.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = scanToken; + if (fixerState.missing === 0) { + fixerState.fixed = 0; + } + } + finally { + updateFixerStatus(); + } + }, 0); + }; + const isEditableTarget = (target) => { + if (!target) + return false; + const tag = String(target.tagName || "").toLowerCase(); + return target.isContentEditable || tag === "input" || tag === "textarea" || tag === "select"; + }; + container.addEventListener("keydown", (event) => { + const key = String(event.key || "").toLowerCase(); + if (event.altKey && !event.ctrlKey && !event.metaKey) { + if (key === "a") { + event.preventDefault(); + createApplyBtn.click(); + } + else if (key === "p") { + event.preventDefault(); + patchNowBtn.click(); + } + } + }); + const resolveModel = () => { + if (useCustomCb.checked) { + const tag = token3Tag(customModelInput.value, true); + return { + modelRaw: customModelInput.value || "Unknown", + modelUpper: true, + tag: tag || "UNKNOWN", + }; + } + if (state.detectedModelTag) { + return { + modelRaw: state.detectedModelTag, + modelUpper: false, + tag: state.detectedModelTag, + }; + } + const sel = modelSelect.value || "Unknown"; + const tag = token3Tag(sel, false); + return { modelRaw: sel, modelUpper: false, tag: tag || "Unknown" }; + }; + const buildWorkflowName = () => { + const modelInfo = resolveModel(); + const modelTag = modelInfo.tag || "Model"; + const assetName = titlePathJS(nameInput.value || "Asset"); + return `${yymmddJS()}_${modelTag}_${assetName}`; + }; + const buildWorkflowPreview = () => { + const base = buildWorkflowName(); + const next = base && state.workflowLastBase === base + ? Number(state.workflowNextSuffix || 1) + : 1; + const suffix = String(next).padStart(4, "0"); + return `${base}_${suffix}.json`; + }; + let existingNamesToken = 0; + let lastExistingKey = ""; + const refreshExistingNames = async (force = false) => { + const projectId = state.projectId || ""; + const media = mediaSelect.value || "images"; + const key = `${projectId}::${media}`; + if (!force && key === lastExistingKey) + return; + lastExistingKey = key; + existingNamesSelect.innerHTML = ""; + const emptyOpt = document.createElement("option"); + emptyOpt.value = ""; + emptyOpt.textContent = "-- Select existing name --"; + existingNamesSelect.appendChild(emptyOpt); + if (!projectId) { + existingNamesSelect.style.display = "none"; + return; + } + const requestToken = (existingNamesToken += 1); + try { + const names = await listExistingNames(projectId, media); + if (requestToken !== existingNamesToken) + return; + if (!names.length) { + existingNamesSelect.style.display = "none"; + return; + } + for (const name of names) { + const opt = document.createElement("option"); + opt.value = name; + opt.textContent = name; + existingNamesSelect.appendChild(opt); + } + if (nameInput.value && names.includes(nameInput.value)) { + existingNamesSelect.value = nameInput.value; + } + else { + existingNamesSelect.value = ""; + } + existingNamesSelect.style.display = "block"; + } + catch (_) { + if (requestToken !== existingNamesToken) + return; + existingNamesSelect.style.display = "none"; + } + }; + let previewToken = 0; + const updatePreview = async () => { + const baseRel = state.projectFolder + ? `PROJECTS/${state.projectFolder}` + : "PROJECTS/"; + const modelInfo = resolveModel(); + const tokens = { + BASE: baseRel, + MEDIA: mediaDir(mediaSelect.value), + DATE: yymmddJS(), + MODEL: String(modelInfo.tag || ""), + NAME: titlePathJS(nameInput.value || "Name"), + KIND: makeKindToken(kindSelect.value), + }; + const template = String(templateInput.value || "").trim(); + const requestToken = (previewToken += 1); + detectedModelLabel.textContent = `Detected: ${state.detectedModelTag || "-"}`; + customModelPreview.textContent = useCustomCb.checked + ? `Custom tag: ${modelInfo.tag}` + : "Custom tag: -"; + if (modelSummary) { + const activeTag = modelInfo.tag || "-"; + modelSummary.textContent = `MODEL (${activeTag})`; + } + const assetFolder = titlePathJS(workflowAssetInput.value || ""); + const workflowFile = buildWorkflowPreview(); + const workflowRel = assetFolder + ? `03_WORKFLOWS/${assetFolder}/${workflowFile}` + : `03_WORKFLOWS/${workflowFile}`; + workflowPreviewLabel.textContent = `Next save: ${workflowRel}`; + modelSelect.disabled = !!state.detectedModelTag && !useCustomCb.checked; + modelSelect.style.opacity = modelSelect.disabled ? "0.6" : "1"; + if (!template) { + previewLabel.textContent = "Preview: -"; + return; + } + previewLabel.textContent = "Preview: ..."; + try { + const resp = await previewTemplate(template, tokens); + if (requestToken !== previewToken) + return; + const preview = resp?.preview || ""; + previewLabel.textContent = preview ? `Preview: ${preview}` : "Preview: -"; + } + catch (err) { + if (requestToken !== previewToken) + return; + const previewError = String(err?.message || err || "Invalid template"); + previewLabel.textContent = `Preview: ${previewError}`; + } + }; + const updateState = () => { + state.workflowName = workflowNameInput.value; + state.workflowAsset = workflowAssetInput.value; + state.kind = kindSelect.value; + state.media = mediaSelect.value; + state.name = nameInput.value; + state.customModelText = customModelInput.value; + state.modelSelection = modelSelect.value; + state.autoPatch = autoPatchCb.checked; + state.autoPatchNewNodes = autoPatchNewCb.checked; + state.autoSwitchTrusted = autoSwitchCb.checked; + state.autoHookSave = autoHookCb.checked; + state.useCustomModel = useCustomCb.checked; + state.modelUpper = useCustomCb.checked; + state.pendingProjectName = pendingProjectNameInput.value; + state.pendingSelectProjectId = pendingSelect.value; + state.pathTemplate = templateInput.value; + customModelWrap.style.display = useCustomCb.checked ? "block" : "none"; + saveState(state); + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + updatePreview(); + if (!state.lastRelDir) { + targetLabel.textContent = "Target: none (create an output)"; + } + }; + const setWorkflowName = (value) => { + workflowNameInput.value = value || ""; + state.workflowName = workflowNameInput.value; + saveState(state); + updatePreview(); + }; + const resetForProjectChange = () => { + state.projectName = ""; + state.workflowName = ""; + state.workflowAsset = ""; + state.workflowLastBase = ""; + state.workflowNextSuffix = 1; + state.workflowPanelOpen = false; + state.name = ""; + state.kind = "asset"; + state.media = "images"; + state.modelSelection = ""; + state.useCustomModel = false; + state.customModelText = ""; + state.modelUpper = false; + state.lastRelDir = ""; + state.lastPrefix = ""; + state.pathTemplate = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + workflowNameInput.value = ""; + workflowAssetInput.value = ""; + nameInput.value = ""; + kindSelect.value = "asset"; + mediaSelect.value = "images"; + if (modelSelect.options.length > 0) { + modelSelect.selectedIndex = 0; + } + useCustomCb.checked = false; + customModelInput.value = ""; + customModelWrap.style.display = "none"; + templateInput.value = state.pathTemplate; + targetLabel.textContent = "Target: none (create an output)"; + updateState(); + updatePreview(); + }; + // Assign UI helper functions after all are defined + state._ui = { + updateStatus, + updateResolve, + updatePreview, + updateWorkflowBlock, + updateTargetLabel, + resetForProjectChange, + buildWorkflowName, + setWorkflowName, + refreshExistingNames, + refreshProjectsList: (force = false) => refreshProjectsList(force), + refreshMissingStatus, + }; + // Store full project list for search filtering + let allProjects = []; + let lastProjectsRefreshAt = 0; + let projectRefreshErrorCount = 0; + const updateProjectDropdowns = (filteredList) => { + pendingSelect.innerHTML = ""; + const pendingEmpty = document.createElement("option"); + pendingEmpty.value = ""; + pendingEmpty.textContent = "-"; + pendingSelect.appendChild(pendingEmpty); + for (const p of filteredList) { + const opt = document.createElement("option"); + opt.value = p.project_id; + opt.dataset.folder = p.folder || ""; + opt.dataset.exists = p.exists === false ? "false" : "true"; + const missingTag = p.exists === false ? " (missing)" : ""; + const archivedTag = p.archived ? " [archived]" : ""; + opt.textContent = `${p.folder || p.project_id} (${p.project_id})${missingTag}${archivedTag}`; + pendingSelect.appendChild(opt); + } + }; + refreshProjectsList = async (force = false) => { + const now = Date.now(); + // Apply exponential backoff on errors + const throttleMs = projectRefreshErrorCount > 0 + ? PROJECT_REFRESH_ERROR_BACKOFF_MS * Math.min(projectRefreshErrorCount, 5) + : PROJECT_REFRESH_THROTTLE_MS; + if (!force && now - lastProjectsRefreshAt < throttleMs) + return; + lastProjectsRefreshAt = now; + try { + const resp = await fetchJSON("/mjr_project/list"); + allProjects = resp.projects || []; + state.lastError = ""; + projectRefreshErrorCount = 0; // Reset error count on success + updateProjectDropdowns(allProjects); + if (state.projectId) { + const active = allProjects.find((p) => p.project_id === state.projectId); + state.projectExists = active ? active.exists !== false : false; + } + if (state.pendingSelectProjectId) { + pendingSelect.value = state.pendingSelectProjectId; + } + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + } + catch (e) { + projectRefreshErrorCount++; + const errorMsg = String(e.message || e); + state.lastError = errorMsg; + updateStatus(); + // Only show toast on first error to avoid spam + if (projectRefreshErrorCount === 1) { + toast("error", "Projects list failed", errorMsg); + } + console.error("[mjr] Project list refresh failed:", e); + } + }; + workflowNameInput.addEventListener("input", updateState); + workflowAssetInput.addEventListener("input", updateState); + kindSelect.addEventListener("change", updateState); + mediaSelect.addEventListener("change", () => { + updateState(); + refreshExistingNames(true); + }); + nameInput.addEventListener("input", updateState); + existingNamesSelect.addEventListener("change", () => { + nameInput.value = existingNamesSelect.value || ""; + updateState(); + }); + customModelInput.addEventListener("input", updateState); + useCustomCb.addEventListener("change", updateState); + autoPatchCb.addEventListener("change", updateState); + autoPatchNewCb.addEventListener("change", updateState); + autoSwitchCb.addEventListener("change", updateState); + autoHookCb.addEventListener("change", updateState); + templateInput.addEventListener("input", updateState); + pendingProjectNameInput.addEventListener("input", updateState); + pendingSelect.addEventListener("change", updateState); + const runFixMissingModels = async () => { + debug("[MJR] Fix Missing Models clicked"); + const originalLabel = fixMissingBtn.textContent; + fixMissingBtn.disabled = true; + fixMissingBtn.textContent = "Scanning..."; + try { + debug("[MJR] Scanning for missing models..."); + const stats = scanMissingModelsWithStats(); + debug("[MJR] Scan results:", stats); + const missing = stats.missing; + fixerState.missing = missing.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = 0; + updateFixerStatus(); + if (!missing.length) { + debug("[MJR] No missing models found"); + toast("info", "No missing models", "All model widgets match available options."); + return; + } + const payload = missing.map((item) => ({ + missing_value: item.missing_value, + type_hint: item.type_hint, + })); + debug("[MJR] Sending scan request to API:", payload); + const resp = await scanModelCandidates(payload); + debug("[MJR] API response:", resp); + const results = Array.isArray(resp?.results) ? resp.results : []; + const resultMap = new Map(); + for (const entry of results) { + const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; + resultMap.set(key, Array.isArray(entry?.candidates) ? entry.candidates : []); + } + const autoFixes = []; + const autoFixedKeys = new Set(); + for (const entry of missing) { + const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; + const candidates = resultMap.get(key) || []; + const exact = candidates.filter((c) => Number(c?.score || 0) === 100 && !c?.in_wrong_folder && c?.reason !== "wrong_folder"); + if (exact.length === 1) { + const relpath = String(exact[0]?.relpath || ""); + if (relpath) { + autoFixes.push({ + node_id: entry?.node_id, + widget_name: entry?.widget_name, + new_value: relpath, + }); + autoFixedKeys.add(`${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`); + } + } + } + let fixedCount = 0; + let remaining = missing.filter((entry) => { + const key = `${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`; + return !autoFixedKeys.has(key); + }); + if (autoFixes.length) { + fixedCount = applyFixesToGraph(autoFixes); + const postStats = scanMissingModelsWithStats(); + remaining = postStats.missing; + fixerState.missing = remaining.length; + fixerState.total = postStats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = fixedCount; + updateFixerStatus(); + } + if (!remaining.length) { + if (fixedCount > 0) { + toast("success", "Fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); + } + else { + toast("info", "No missing models", "All model widgets match available options."); + } + return; + } + const fixes = await showModelFixerDialog({ + missing: remaining, + results, + }); + debug("[MJR] User selected fixes:", fixes); + if (!fixes) { + if (fixedCount > 0) { + toast("success", "Auto-fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); + } + else { + toast("info", "Fix cancelled", "No changes applied."); + } + return; + } + const fixed = applyFixesToGraph(fixes); + debug("[MJR] Applied fixes count:", fixed); + const postStats = scanMissingModelsWithStats(); + fixerState.missing = postStats.missing.length; + fixerState.total = postStats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = fixedCount + fixed; + updateFixerStatus(); + if (fixed + fixedCount > 0) { + toast("success", "Fix applied", `Updated ${fixed + fixedCount} widget${fixed + fixedCount !== 1 ? "s" : ""}.`); + } + else { + toast("warn", "No changes", "No widgets were updated."); + } + } + catch (e) { + console.error("[MJR] Fix missing models error:", e); + toast("error", "Fix failed", String(e?.message || e)); + } + finally { + fixMissingBtn.textContent = originalLabel; + fixMissingBtn.disabled = false; + } + }; + const uniqueMissingForDownload = (missing) => { + const seen = new Set(); + const out = []; + for (const item of missing || []) { + const key = `${item.missing_value}::${item.type_hint || ""}`; + if (seen.has(key)) + continue; + seen.add(key); + out.push(item); + } + return out; + }; + const basename = (value) => normalizeKey(value); + const pollDownloadJob = async (jobId) => { + while (true) { + try { + const status = await getDownloadStatus(jobId); + downloadState.state = status?.state || "downloading"; + downloadState.message = status?.message || ""; + downloadState.progress = status?.progress || { current: 0, total: 0, pct: 0 }; + updateDownloadStatus(); + if (downloadState.state === "done" || downloadState.state === "error") { + return status; + } + } + catch (e) { + downloadState.state = "error"; + downloadState.message = "status check failed"; + updateDownloadStatus(); + return null; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + }; + const runDownloadMissingModels = async () => { + debug("[MJR] Download Missing Models clicked"); + if (downloadState.state === "downloading") { + debug("[MJR] Download already in progress"); + toast("warn", "Download in progress", "Please wait for the current job to finish."); + return; + } + const originalLabel = downloadMissingBtn.textContent; + downloadMissingBtn.disabled = true; + downloadMissingBtn.textContent = "Scanning..."; + downloadState.state = "idle"; + downloadState.message = "scanning"; + updateDownloadStatus(); + try { + debug("[MJR] Scanning for missing models..."); + const stats = scanMissingModelsWithStats(); + debug("[MJR] Scan results:", stats); + const missingAll = stats.missing; + const missing = uniqueMissingForDownload(missingAll); + debug("[MJR] Unique missing for download:", missing); + fixerState.missing = missingAll.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + updateFixerStatus(); + if (!missing.length) { + debug("[MJR] No missing models found"); + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + toast("info", "No missing models", "All model widgets match available options."); + return; + } + const payload = missing.map((item) => ({ + missing_value: item.missing_value, + type_hint: item.type_hint, + })); + const workflowJson = getSerializedWorkflow(); + const noteTexts = extractNoteTexts(workflowJson); + const noteRecipes = collectNoteRecipes(workflowJson); + const resolved = await resolveModelRecipes(payload); + const resolvedMap = new Map(); + for (const entry of resolved?.resolved || []) { + const key = basename(entry.key || entry.missing_value); + resolvedMap.set(key, entry); + } + const kindOptions = getKindOptions(); + let existingMap = null; + try { + const modelResp = await getModels(); + const categories = modelResp?.categories || {}; + existingMap = new Map(); + for (const [kind, items] of Object.entries(categories)) { + if (!Array.isArray(items)) + continue; + const set = new Set(); + for (const item of items) { + const raw = String(item || "").replace(/\\/g, "/").toLowerCase(); + if (!raw) + continue; + set.add(raw); + const base = raw.split("/").pop(); + if (base) + set.add(base); + } + existingMap.set(kind, set); + } + } + catch (_) { + existingMap = null; + } + // Helper to check if model exists + const modelExists = (kind, filename) => { + if (!existingMap || !kind || !filename) + return false; + const set = existingMap.get(kind); + if (!set) + return false; + const normalized = String(filename || "").replace(/\\/g, "/").toLowerCase(); + return set.has(normalized) || set.has(normalized.split("/").pop()); + }; + let matches = 0; + const allEntries = missing.map((item) => { + const key = basename(item.missing_value); + const stored = resolvedMap.get(key) || {}; + const fallbackKind = stored.kind || typeHintToKind(item.type_hint); + const noteRecipe = noteRecipes.get(key); + let recipe = stored.recipe || null; + let prefilled = false; + if (noteRecipe) { + recipe = noteRecipe; + if (recipe?.kind === "unknown" && fallbackKind) { + recipe = { ...recipe, kind: fallbackKind }; + } + prefilled = true; + matches += 1; + } + else if (recipe) { + recipe = { ...recipe, source: "stored" }; + prefilled = true; + } + const kind = stored.kind || fallbackKind; + const filename = recipe?.filename || key; + const exists = modelExists(kind, filename); + return { + missing_value: item.missing_value, + key, + kind, + recipe, + prefilled, + exists, // Flag to track if model already downloaded + }; + }); + // Filter to only show truly missing models (not already downloaded) + const dialogEntries = allEntries.filter((entry) => !entry.exists); + const alreadyDownloaded = allEntries.length - dialogEntries.length; + const entryMap = new Map(dialogEntries.map((entry) => [entry.key, entry])); + console.debug("[mjr] download filter", { + total: allEntries.length, + missing: dialogEntries.length, + alreadyDownloaded, + notes: noteTexts.length, + recipes: noteRecipes.size, + matches, + }); + if (dialogEntries.length === 0) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + if (alreadyDownloaded > 0) { + toast("success", "All models downloaded", `${alreadyDownloaded} model${alreadyDownloaded !== 1 ? 's' : ''} already in models folder.`); + } + else { + toast("info", "No downloads needed", "No models to download."); + } + return; + } + const dialogResult = await showModelDownloaderDialog({ + entries: dialogEntries, + kindOptions, + existingMap, + }); + if (!dialogResult) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + return; + } + const items = dialogResult.items || []; + if (!items.length) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + toast("warn", "No downloads", "Select at least one entry to download."); + return; + } + const autoRememberNotes = getSettingBool(SETTING_AUTO_REMEMBER_NOTE_SOURCES, true); + const saveItems = []; + if (dialogResult.remember && dialogResult.saveItems?.length) { + saveItems.push(...dialogResult.saveItems); + } + if (autoRememberNotes) { + for (const item of items) { + const entry = entryMap.get(item.key); + if (entry?.recipe?.source === "workflow_note") { + saveItems.push(item); + } + } + } + if (saveItems.length) { + const seenKeys = new Set(); + const deduped = []; + for (const item of saveItems) { + if (!item || !item.key) + continue; + if (seenKeys.has(item.key)) + continue; + seenKeys.add(item.key); + deduped.push(item); + } + if (deduped.length) { + await saveModelRecipes(deduped); + } + } + const resp = await downloadModels(items); + const jobId = resp?.job_id; + if (!jobId) { + throw new Error("download job not created"); + } + downloadState.state = "downloading"; + downloadState.message = "queued"; + downloadState.progress = { current: 0, total: 0, pct: 0 }; + updateDownloadStatus(); + downloadMissingBtn.textContent = "Downloading..."; + const finalStatus = await pollDownloadJob(jobId); + const summary = finalStatus?.summary || {}; + const downloaded = Number(summary.downloaded || 0); + const errors = Number(summary.errors || 0); + const skipped = Number(summary.skipped || 0); + if (downloadState.state === "error") { + toast("error", "Download finished", `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}`); + } + else { + toast("success", "Download finished", `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}`); + } + try { + loadModels(); + } + catch (_) { } + try { + app.graph?.setDirtyCanvas?.(true, true); + } + catch (_) { } + const autoFix = getSettingBool(SETTING_AUTO_FIX_AFTER_DOWNLOAD, true); + if (autoFix) { + await runFixMissingModels(); + } + else { + const runFix = await psConfirm({ + title: "Run Fix Missing Models?", + message: "Downloads finished. Run Fix Missing Models now?", + }); + if (runFix) { + await runFixMissingModels(); + } + else { + toast("info", "Fix skipped", "Re-open model dropdowns if they do not refresh."); + } + } + } + catch (e) { + downloadState.state = "error"; + downloadState.message = "download failed"; + updateDownloadStatus(); + toast("error", "Download failed", String(e?.message || e)); + } + finally { + downloadMissingBtn.textContent = originalLabel; + downloadMissingBtn.disabled = false; + } + }; + fixMissingBtn.addEventListener("click", runFixMissingModels); + downloadMissingBtn.addEventListener("click", runDownloadMissingModels); + buildFingerprintBtn.addEventListener("click", async () => { + const originalLabel = buildFingerprintBtn.textContent; + buildFingerprintBtn.disabled = true; + buildFingerprintBtn.textContent = "Building..."; + try { + const resp = await buildFingerprintCache({}); + const count = Number(resp?.count || 0); + fixerState.cacheCount = count; + fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); + fixerState.cacheError = ""; + updateFixerStatus(); + const hashed = Number(resp?.hashed || 0); + const reused = Number(resp?.reused || 0); + toast("success", "Cache built", `Items: ${count} | Hashed: ${hashed} | Reused: ${reused}`); + } + catch (e) { + toast("error", "Cache build failed", String(e?.message || e)); + } + finally { + buildFingerprintBtn.textContent = originalLabel; + buildFingerprintBtn.disabled = false; + refreshFingerprintStatus(); + } + }); + saveWorkflowBtn.addEventListener("click", async () => { + updateState(); + const wfName = workflowNameInput.value.trim(); + await saveWorkflowToProject(state, wfName); + }); + switchBtn.addEventListener("click", async () => { + const det = state.detected; + if (!det || det.mode === "none") { + toast("warn", "No detected project", "Nothing to switch to."); + return; + } + try { + if (det.project_id) { + await setActiveProjectById(state, det.project_id); + } + else if (det.project_folder) { + const resp = await fetchJSON("/mjr_project/set", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ project_name: det.project_folder, create_base: true }), + }); + state.projectId = resp.project_id; + state.projectFolder = resp.project_folder; + state.projectExists = true; + state.lastError = ""; + saveState(state); + resetForProjectChange(); + refreshProjectsList(true); + toast("success", "Project active", resp.project_folder); + } + else { + toast("warn", "No detected project", "No folder available to switch."); + return; + } + state.detected = null; + updateResolve(); + state.workflowHasSignature = true; + updateWorkflowBlock(); + stampGraphProjectSignature(app, state); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Switch failed", state.lastError); + } + }); + const assignWorkflowToActive = async () => { + if (!state.projectId || !state.projectFolder) { + toast("error", "No active project", "Set a project first."); + return; + } + stampGraphProjectSignature(app, state); + state.workflowHasSignature = true; + state.workflowPanelOpen = false; + toast("success", "Workflow assigned", state.projectFolder); + const doPatch = await psConfirm({ + title: "Patch save paths?", + message: "Patch save paths to the active project?", + }); + if (doPatch) { + const base = `PROJECTS/${state.projectFolder}`; + const imgRel = joinRel(base, mediaDir("images")); + const vidRel = joinRel(base, mediaDir("videos")); + const patchedImages = await patchSaveNodes(app, imgRel, "", "images"); + const patchedVideos = await patchSaveNodes(app, vidRel, "", "videos"); + const patched = patchedImages + patchedVideos; + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} nodes.`); + } + else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + } + else { + toast("info", "Patch skipped", "Save paths were not changed."); + } + state.detected = null; + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + }; + assignBtn.addEventListener("click", assignWorkflowToActive); + createBtn.addEventListener("click", async () => { + const det = state.detected; + const suggested = det?.project_folder || "New_Project"; + const entered = await psPrompt({ + title: "Create project", + message: "Enter a project name", + defaultValue: suggested, + }); + if (entered === null) { + toast("info", "Create cancelled", "No project name provided."); + return; + } + const projectName = String(entered || "").trim(); + if (!projectName) { + toast("error", "Validation", "Project name is required."); + return; + } + try { + await createAndActivateProject(state, projectName, { + updateStatus, + updateResolve, + updateWorkflowBlock, + resetForProjectChange, + updatePreview, + refreshProjectsList, + }); + state.detected = null; + updateResolve(); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + } + }); + dismissBtn.addEventListener("click", () => { + state.detected = null; + updateResolve(); + }); + wfCreateBtn.addEventListener("click", async () => { + const name = pendingProjectNameInput.value.trim(); + if (!name) { + toast("error", "Validation", "Project name is required."); + return; + } + try { + await createAndActivateProject(state, name, { + updateStatus, + updateResolve, + updateWorkflowBlock, + resetForProjectChange, + updatePreview, + refreshProjectsList, + }); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + } + }); + wfActivateBtn.addEventListener("click", async () => { + const projectId = pendingSelect.value; + if (!projectId) { + toast("error", "Validation", "Select a project first."); + return; + } + try { + await setActiveProjectById(state, projectId); + state.workflowHasSignature = true; + updateWorkflowBlock(); + stampGraphProjectSignature(app, state); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Activate failed", state.lastError); + } + }); + wfAssignBtn.addEventListener("click", assignWorkflowToActive); + modelSelect.addEventListener("change", updateState); + const loadModels = async () => { + try { + const resp = await fetchJSON("/mjr_project/models"); + const categories = resp.categories || {}; + modelSelect.innerHTML = ""; + const primaryCats = ["diffusion_models", "checkpoints"]; + const extraCats = Object.keys(categories).filter((c) => !primaryCats.includes(c)); + const addGroup = (cat) => { + const items = categories[cat] || []; + if (!Array.isArray(items) || items.length === 0) + return 0; + const group = document.createElement("optgroup"); + group.label = cat; + for (const item of items) { + const opt = document.createElement("option"); + opt.value = item; + opt.textContent = item; + group.appendChild(opt); + } + modelSelect.appendChild(group); + return items.length; + }; + let total = 0; + for (const cat of primaryCats) { + total += addGroup(cat); + } + if (state.showMoreModels) { + for (const cat of extraCats) { + total += addGroup(cat); + } + } + if (total === 0) { + const opt = document.createElement("option"); + opt.value = "Unknown"; + opt.textContent = "Unknown"; + modelSelect.appendChild(opt); + } + modelEmptyMsg.style.display = total === 0 ? "block" : "none"; + if (state.modelSelection) { + modelSelect.value = state.modelSelection; + } + if (!modelSelect.value && modelSelect.options.length > 0) { + modelSelect.selectedIndex = 0; + } + updateState(); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Model list failed", state.lastError); + console.error(e); + } + }; + modelToggleBtn.addEventListener("click", () => { + state.showMoreModels = !state.showMoreModels; + modelToggleBtn.textContent = state.showMoreModels ? "Show less" : "Show more"; + saveState(state); + loadModels(); + }); + createApplyBtn.addEventListener("click", async () => { + if (!state.projectId) { + state.lastError = "set project first"; + updateStatus(); + toast("error", "Validation", "Set a project first."); + return; + } + const name = nameInput.value.trim(); + if (!name) { + state.lastError = "name required"; + updateStatus(); + toast("error", "Validation", "Name is required."); + return; + } + if (useCustomCb.checked && !customModelInput.value.trim()) { + toast("error", "Validation", "Custom model is required."); + return; + } + const modelInfo = resolveModel(); + const modelName = modelInfo.modelRaw || "Unknown"; + try { + const resp = await fetchJSON("/mjr_project/create_custom_out", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + project_id: state.projectId, + kind: kindSelect.value, + name, + media: mediaSelect.value, + model: modelName, + model_upper: modelInfo.modelUpper === true, + date: yymmddJS(), + template: templateInput.value, + }), + }); + state.lastRelDir = resp.rel_dir; + state.lastPrefix = resp.filename_prefix; + state.media = resp.media || mediaSelect.value; + state.kind = resp.kind || kindSelect.value; + state.projectExists = true; + state.workflowHasSignature = true; + state.lastError = ""; + saveState(state); + updateStatus(); + updateWorkflowBlock(); + targetLabel.textContent = `Target: ${resp.rel_dir} | Prefix: ${resp.filename_prefix}`; + if (autoPatchCb.checked) { + const patched = await patchSaveNodes(app, resp.rel_dir, resp.filename_prefix, resp.media); + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} nodes.`); + } + else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + } + stampGraphProjectSignature(app, state); + toast("success", "Folder created", resp.rel_dir); + refreshProjectsList(true); + } + catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + console.error(e); + } + }); + patchNowBtn.addEventListener("click", async () => { + rollOutputDateIfNeeded(); + if (!state.lastRelDir || !state.lastPrefix) { + targetLabel.textContent = "Target: none (create an output)"; + toast("warn", "No target", "Create an output first."); + return; + } + // Count save nodes first + const saveNodes = (app.graph?._nodes || []).filter(n => isSaveLikeNode(n)); + const nodeCount = saveNodes.length; + if (nodeCount === 0) { + toast("warn", "No nodes to patch", "No save-like nodes found in workflow."); + return; + } + // Ask for confirmation + const confirmed = await psConfirm({ + title: "Patch save paths?", + message: `Update ${nodeCount} save node${nodeCount !== 1 ? "s" : ""} to use the project path?\n\n` + + `Path: ${state.lastRelDir}\n` + + `Prefix: ${state.lastPrefix}\n\n` + + `This can be undone with Ctrl+Z.`, + }); + if (!confirmed) { + toast("info", "Cancelled", "No nodes were modified."); + return; + } + const patched = await patchSaveNodes(app, state.lastRelDir, state.lastPrefix, state.media); + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} node${patched !== 1 ? 's' : ''}.`); + } + else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + stampGraphProjectSignature(app, state); + }); + loadModels(); + refreshProjectsList(true); + refreshExistingNames(true); + updatePreview(); + refreshFingerprintStatus(); + if (getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true)) { + refreshMissingStatus(true); + } + else { + updateFixerStatus(); + } } +//# sourceMappingURL=ui_components.js.map \ No newline at end of file diff --git a/js/ui_components.js.map b/js/ui_components.js.map new file mode 100644 index 0000000..06dfdb6 --- /dev/null +++ b/js/ui_components.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ui_components.js","sourceRoot":"","sources":["../src/ui_components.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3fd21b5 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..811267e --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/majoor_project_settings.ts b/src/majoor_project_settings.ts new file mode 100644 index 0000000..3868acd --- /dev/null +++ b/src/majoor_project_settings.ts @@ -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//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); + }, +}); diff --git a/src/mjr/api.ts b/src/mjr/api.ts new file mode 100644 index 0000000..b889b59 --- /dev/null +++ b/src/mjr/api.ts @@ -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 || {}), + }); +} diff --git a/src/mjr/dialog.ts b/src/mjr/dialog.ts new file mode 100644 index 0000000..e9fdb0d --- /dev/null +++ b/src/mjr/dialog.ts @@ -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?"); +} diff --git a/src/mjr/graph.ts b/src/mjr/graph.ts new file mode 100644 index 0000000..d19c26f --- /dev/null +++ b/src/mjr/graph.ts @@ -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; + } +} diff --git a/src/mjr/log.ts b/src/mjr/log.ts new file mode 100644 index 0000000..bd11a28 --- /dev/null +++ b/src/mjr/log.ts @@ -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); +} diff --git a/src/mjr/model_contribution_dialog.ts b/src/mjr/model_contribution_dialog.ts new file mode 100644 index 0000000..bc2d653 --- /dev/null +++ b/src/mjr/model_contribution_dialog.ts @@ -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 = ` +

Share a model URL

+

Help others by contributing a verified download link.

+ `; + + 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); +} diff --git a/src/mjr/model_downloader.ts b/src/mjr/model_downloader.ts new file mode 100644 index 0000000..6136669 --- /dev/null +++ b/src/mjr/model_downloader.ts @@ -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); +} diff --git a/src/mjr/model_fixer.ts b/src/mjr/model_fixer.ts new file mode 100644 index 0000000..e9f9b57 --- /dev/null +++ b/src/mjr/model_fixer.ts @@ -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; +} diff --git a/src/mjr/patch.ts b/src/mjr/patch.ts new file mode 100644 index 0000000..3c4a52f --- /dev/null +++ b/src/mjr/patch.ts @@ -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(), + }; +} diff --git a/src/mjr/toast.ts b/src/mjr/toast.ts new file mode 100644 index 0000000..45281d9 --- /dev/null +++ b/src/mjr/toast.ts @@ -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 (_) {} +} diff --git a/src/mjr/ui_model_downloader_dialog.ts b/src/mjr/ui_model_downloader_dialog.ts new file mode 100644 index 0000000..99291bd --- /dev/null +++ b/src/mjr/ui_model_downloader_dialog.ts @@ -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); + }); +} diff --git a/src/mjr/ui_model_fixer_dialog.ts b/src/mjr/ui_model_fixer_dialog.ts new file mode 100644 index 0000000..0453b91 --- /dev/null +++ b/src/mjr/ui_model_fixer_dialog.ts @@ -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); + }); +} diff --git a/src/mjr/utils.ts b/src/mjr/utils.ts new file mode 100644 index 0000000..0998595 --- /dev/null +++ b/src/mjr/utils.ts @@ -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("/"); +} diff --git a/src/mjr/workflow_note_recipes.ts b/src/mjr/workflow_note_recipes.ts new file mode 100644 index 0000000..2b654e6 --- /dev/null +++ b/src/mjr/workflow_note_recipes.ts @@ -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; +} diff --git a/src/state_manager.ts b/src/state_manager.ts new file mode 100644 index 0000000..94fe3f1 --- /dev/null +++ b/src/state_manager.ts @@ -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 (_) {} +} diff --git a/src/types/comfyui.d.ts b/src/types/comfyui.d.ts new file mode 100644 index 0000000..e656ec7 --- /dev/null +++ b/src/types/comfyui.d.ts @@ -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; +} diff --git a/src/ui_components.ts b/src/ui_components.ts new file mode 100644 index 0000000..0ef1057 --- /dev/null +++ b/src/ui_components.ts @@ -0,0 +1,2391 @@ +import { app } from "../../scripts/app.js"; +import { joinRel, makeKindToken, mediaDir, titlePathJS, token3Tag, yymmddJS } from "./mjr/utils.js"; +import { ensureStyles, toast } from "./mjr/toast.js"; +import { + buildFingerprintCache, + downloadModels, + fetchJSON, + getDownloadStatus, + getFingerprintCacheStatus, + getModels, + listExistingNames, + previewTemplate, + resolveModelRecipes, + saveModelRecipes, + scanModelCandidates, + searchRegistry, +} from "./mjr/api.js"; +import { psConfirm, psPrompt } from "./mjr/dialog.js"; +import { + alreadyProjectPathed, + detectNodeMedia, + isSaveLikeNode, + patchSaveNodes, + patchSingleNode, + stampGraphProjectSignature, +} from "./mjr/patch.js"; +import { + applyFixesToGraph, + scanMissingModelsFromGraph, + scanMissingModelsWithStats, +} from "./mjr/model_fixer.js"; +import { collectNoteRecipes, getKindOptions, normalizeKey, typeHintToKind } from "./mjr/model_downloader.js"; +import { showModelFixerDialog } from "./mjr/ui_model_fixer_dialog.js"; +import { showModelDownloaderDialog } from "./mjr/ui_model_downloader_dialog.js"; +import { showModelContributionDialog } from "./mjr/model_contribution_dialog.js"; +import { saveState } from "./state_manager.js"; +import { getSerializedWorkflow } from "./mjr/graph.js"; +import { extractNoteTexts } from "./mjr/workflow_note_recipes.js"; +import { debug } from "./mjr/log.js"; + +const TEMPLATE_TOKENS = ["{BASE}", "{MEDIA}", "{DATE}", "{MODEL}", "{NAME}", "{KIND}"]; + +const PROJECT_REFRESH_THROTTLE_MS = 5000; +const PROJECT_REFRESH_ERROR_BACKOFF_MS = 10000; +const SETTING_AUTO_FIX_AFTER_DOWNLOAD = "mjr_project.auto_fix_after_download"; +const SETTING_AUTO_SCAN_MISSING_ON_OPEN = "mjr_project.auto_scan_missing_on_open"; +const SETTING_AUTO_REMEMBER_NOTE_SOURCES = "mjr_project.auto_remember_note_sources"; +const SETTING_NO_PROJECT_STATUS_BLACK = "mjr_project.status_black_no_project"; + +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?.ui?.settings?.settingsValues?.[id]; + return toBool(raw, fallback); +}; + +function parseHexColor(hex) { + const m = String(hex || "").trim().match(/^#([0-9a-fA-F]{6})$/); + if (!m) return null; + const v = m[1]; + return [ + parseInt(v.slice(0, 2), 16), + parseInt(v.slice(2, 4), 16), + parseInt(v.slice(4, 6), 16), + ]; +} + +function colorWithAlpha(hex, alpha) { + const rgb = parseHexColor(hex); + if (!rgb) return hex; + const a = Math.max(0, Math.min(1, Number(alpha))); + return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${a})`; +} + +function makeStatus(state) { + const det = state.detected; + const sameProject = + det && + det.mode !== "none" && + ((det.project_id && det.project_id === state.projectId) || + (det.project_folder && det.project_folder === state.projectFolder)); + const mismatch = + det && + det.mode !== "none" && + !sameProject && + (det.project_id || det.project_folder); + + if (state.lastError) { + return { color: "#b33a3a", text: `Error: ${state.lastError}`, level: "red" }; + } + if (state.graphIsEmpty) { + return { color: "#b33a3a", text: "Empty graph, set or create a project", level: "red" }; + } + if (!state.projectId) { + if (getSettingBool(SETTING_NO_PROJECT_STATUS_BLACK, false)) { + return { color: "#000000", text: "No project (test)", level: "black" }; + } + return { color: "#666666", text: "No active project", level: "gray" }; + } + if (state.projectExists === false) { + return { + color: "#b33a3a", + text: `Missing project folder: ${state.projectFolder || state.projectId}`, + level: "red", + }; + } + if (state.workflowChecked && !state.workflowHasSignature) { + return { + color: "#c77c2a", + text: "Active project, workflow unassigned", + level: "orange", + }; + } + if (mismatch) { + return { + color: "#c77c2a", + text: `Project mismatch: ${det.project_folder || det.project_id}`, + level: "orange", + }; + } + return { + color: "#2f8f4e", + text: `Active: ${state.projectFolder} (${state.projectId})`, + level: "green", + }; +} + +const MENU_BADGE_ID = "mjr-project-status-badge"; +const MENU_ACTION_SELECTORS = [ + ".comfy-menu-actions", + ".comfy-menu-no-drag", + ".comfy-topbar-actions", + ".comfy-menu", + ".workflow-tabs-container", + ".workflow-tabs-container .flex", + ".menu-actions" +]; + +let menuBadgeInterval = null; +let menuBadgePendingState = null; +let menuBadgeLogTimestamp = 0; + +function findMenuActionsContainer() { + for (const selector of MENU_ACTION_SELECTORS) { + const candidate = document.querySelector(selector); + if (candidate) { + return candidate; + } + } + return null; +} + +function ensureMenuStatusBadge(actions, text, color) { + if (!actions) return null; + let badge = document.getElementById(MENU_BADGE_ID); + if (!badge) { + badge = document.createElement("div"); + badge.id = MENU_BADGE_ID; + badge.style.display = "inline-flex"; + badge.style.alignItems = "center"; + badge.style.justifyContent = "center"; + badge.style.padding = "4px 10px"; + badge.style.borderRadius = "999px"; + badge.style.fontSize = "11px"; + badge.style.fontWeight = "600"; + badge.style.border = "1px solid transparent"; + badge.style.marginLeft = "8px"; + badge.style.gap = "4px"; + badge.style.transition = "background 0.3s ease, border 0.3s ease"; + badge.style.cursor = "default"; + actions.appendChild(badge); + } + if (text) badge.textContent = text; + if (color) { + badge.style.background = color; + badge.style.borderColor = color; + } + return badge; +} + +function updateMenuStatusBadge(state) { + menuBadgePendingState = state; + const actions = findMenuActionsContainer(); + if (!actions) { + if (!menuBadgeInterval) { + menuBadgeInterval = setInterval(() => { + if (menuBadgePendingState) { + updateMenuStatusBadge(menuBadgePendingState); + } + }, 500); + } + const now = Date.now(); + if (now - menuBadgeLogTimestamp > 5000) { + console.info( + `[mjr] Waiting for topbar badge target (${MENU_ACTION_SELECTORS.join( + ", " + )}) project=${state?.projectId || "none"} folder=${state?.projectFolder || "none"}` + ); + menuBadgeLogTimestamp = now; + } + console.debug("[mjr] comfy menu actions not found yet, retrying badge soon", state); + return; + } + + if (menuBadgeInterval) { + clearInterval(menuBadgeInterval); + menuBadgeInterval = null; + } + + const badgeText = state.projectId + ? state.projectExists === false + ? "Projet introuvable" + : `Projet: ${state.projectFolder || state.projectId}` + : "Pas de projet actif"; + const badgeColor = state.projectId + ? state.projectExists === false + ? "rgba(179, 58, 58, 0.9)" + : "rgba(47, 143, 78, 0.95)" + : "rgba(102, 102, 102, 0.8)"; + ensureMenuStatusBadge(actions, badgeText, badgeColor); + menuBadgePendingState = null; +} +export function buildPanel(el, state, actions) { + const { createAndActivateProject, setActiveProjectById, saveWorkflowToProject } = actions || {}; + el.innerHTML = ""; + ensureStyles(); + + const UI_GAP = "8px"; + + const container = document.createElement("div"); + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = UI_GAP; + container.style.fontSize = "12px"; + container.style.color = "#ddd"; + container.style.fontFamily = "inherit"; + container.style.padding = "8px 14px 16px"; + + const section = (label) => { + const h = document.createElement("div"); + h.textContent = label; + h.style.fontWeight = "600"; + h.style.marginTop = UI_GAP; + h.style.marginBottom = UI_GAP; + h.style.paddingLeft = "2px"; + h.style.textTransform = "uppercase"; + h.style.letterSpacing = "0.6px"; + h.style.color = "#8fb9ff"; + return h; + }; + + const divider = (thick = false) => { + const d = document.createElement("div"); + d.style.height = thick ? "3px" : "1px"; + d.style.background = "rgba(255,255,255,0.08)"; + d.style.margin = "0"; + return d; + }; + + const detailsSection = (title) => { + const details = document.createElement("details"); + details.style.marginTop = "0"; + details.style.display = "flex"; + details.style.flexDirection = "column"; + details.style.gap = UI_GAP; + const summary = document.createElement("summary"); + summary.textContent = title; + summary.style.cursor = "pointer"; + summary.style.opacity = "0.9"; + summary.style.textTransform = "uppercase"; + summary.style.letterSpacing = "0.6px"; + summary.style.color = "#8fb9ff"; + summary.style.fontWeight = "600"; + summary.style.paddingLeft = "2px"; + details.appendChild(summary); + details._summary = summary; + return details; + }; + + const makeLabel = (labelText) => { + const label = document.createElement("label"); + label.textContent = labelText; + label.style.opacity = "0.85"; + label.style.marginLeft = "2px"; + label.style.fontSize = "11px"; + label.style.lineHeight = "1.2"; + return label; + }; + + const styleField = (inputEl) => { + inputEl.style.width = "100%"; + inputEl.style.boxSizing = "border-box"; + inputEl.style.padding = "6px 8px"; + inputEl.style.borderRadius = "8px"; + inputEl.style.border = "1px solid rgba(255,255,255,0.12)"; + inputEl.style.background = "rgba(0,0,0,0.25)"; + inputEl.style.color = "#eee"; + inputEl.style.outline = "none"; + }; + + const row = (labelText, inputEl) => { + const wrap = document.createElement("div"); + wrap.style.display = "flex"; + wrap.style.flexDirection = "column"; + wrap.style.gap = UI_GAP; + wrap.appendChild(makeLabel(labelText)); + styleField(inputEl); + wrap.appendChild(inputEl); + return wrap; + }; + + const groupRow = (labelText, contentEl) => { + const wrap = document.createElement("div"); + wrap.style.display = "flex"; + wrap.style.flexDirection = "column"; + wrap.style.gap = UI_GAP; + wrap.appendChild(makeLabel(labelText)); + wrap.appendChild(contentEl); + return wrap; + }; + + const btn = (text) => { + const b = document.createElement("button"); + b.textContent = text; + b.className = "mjr-ps-btn"; + b.style.padding = "7px 8px"; + b.style.borderRadius = "8px"; + b.style.border = "1px solid rgba(46, 136, 255, 0.65)"; + b.style.background = "rgba(46, 136, 255, 0.25)"; + b.style.color = "#eee"; + b.style.cursor = "pointer"; + b.style.margin = "0"; + b.style.lineHeight = "1.2"; + b.style.whiteSpace = "nowrap"; + return b; + }; + + // Status bar + const statusBar = document.createElement("div"); + statusBar.style.display = "flex"; + statusBar.style.alignItems = "center"; + statusBar.style.gap = UI_GAP; + statusBar.style.padding = "6px 8px"; + statusBar.style.borderRadius = "8px"; + statusBar.style.position = "sticky"; + statusBar.style.top = "0"; + statusBar.style.zIndex = "2"; + statusBar.style.background = "rgba(10, 12, 16, 0.72)"; + statusBar.style.backdropFilter = "blur(6px)"; + statusBar.style.cursor = "pointer"; + statusBar.title = "Toggle workflow project panel"; + const statusDot = document.createElement("div"); + statusDot.style.width = "8px"; + statusDot.style.height = "8px"; + statusDot.style.borderRadius = "50%"; + const statusText = document.createElement("div"); + statusText.style.opacity = "0.95"; + + const updateStatus = () => { + const s = makeStatus(state); + const level = s.level || "gray"; + statusBar.style.background = colorWithAlpha(s.color, 0.92); + statusBar.style.border = `1px solid ${colorWithAlpha(s.color, 0.35)}`; + statusDot.style.background = s.color; + statusText.textContent = s.text; + if (workflowSaveWrap) { + workflowSaveWrap.style.border = `1px solid ${colorWithAlpha(s.color, 0.4)}`; + workflowSaveWrap.style.background = colorWithAlpha(s.color, 0.12); + } + if (workflowWrap) { + const emphasize = level === "orange" && state.workflowChecked && !state.workflowHasSignature; + workflowWrap.style.border = emphasize + ? `1px solid ${colorWithAlpha("#c77c2a", 0.7)}` + : "1px solid rgba(255,255,255,0.12)"; + workflowWrap.style.background = emphasize + ? colorWithAlpha("#c77c2a", 0.1) + : "rgba(15, 17, 22, 0.75)"; + } + updateMenuStatusBadge(state); + }; + statusBar.appendChild(statusDot); + statusBar.appendChild(statusText); + statusBar.addEventListener("click", () => { + state.workflowPanelOpen = !state.workflowPanelOpen; + saveState(state); + updateWorkflowBlock(); + }); + + const updateResolve = () => { + const det = state.detected; + let show = true; + if (!det || det.mode === "none") { + show = false; + } + const sameProject = + det && + det.mode !== "none" && + ((det.project_id && det.project_id === state.projectId) || + (det.project_folder && det.project_folder === state.projectFolder)); + if (sameProject) { + show = false; + } + if (!show) { + resolveWrap.style.display = "none"; + return; + } + + if (state.workflowPanelOpen) { + state.workflowPanelOpen = false; + saveState(state); + } + + const mode = det.mode || "none"; + const wfFolder = det.project_folder || "none"; + resolveInfo.textContent = `Workflow project: ${wfFolder} (mode: ${mode})`; + resolveActive.textContent = `Active project: ${state.projectFolder || "none"}`; + resolveWrap.style.display = "flex"; + + const canSwitch = mode !== "none" && (det.project_id || det.project_folder); + switchBtn.disabled = !canSwitch; + switchBtn.style.opacity = canSwitch ? "1" : "0.5"; + + const canAssign = !!state.projectId; + assignBtn.disabled = !canAssign; + assignBtn.style.opacity = canAssign ? "1" : "0.5"; + }; + + const resolveWrap = document.createElement("div"); + resolveWrap.style.display = "none"; + resolveWrap.style.flexDirection = "column"; + resolveWrap.style.gap = UI_GAP; + resolveWrap.style.padding = "8px 10px"; + resolveWrap.style.borderRadius = "8px"; + resolveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + resolveWrap.style.background = "rgba(15, 17, 22, 0.75)"; + resolveWrap.style.backdropFilter = "blur(6px)"; + + const resolveTitle = document.createElement("div"); + resolveTitle.textContent = "Workflow project"; + resolveTitle.style.textTransform = "uppercase"; + resolveTitle.style.letterSpacing = "0.6px"; + resolveTitle.style.fontWeight = "600"; + resolveTitle.style.color = "#8fb9ff"; + resolveTitle.style.marginBottom = "0"; + + const resolveInfo = document.createElement("div"); + resolveInfo.style.opacity = "0.9"; + + const resolveActive = document.createElement("div"); + resolveActive.style.opacity = "0.85"; + + const resolveBtns = document.createElement("div"); + resolveBtns.style.display = "flex"; + resolveBtns.style.gap = UI_GAP; + resolveBtns.style.flexWrap = "wrap"; + resolveBtns.style.justifyContent = "center"; + resolveBtns.style.alignItems = "center"; + resolveBtns.style.marginTop = "0"; + + const switchBtn = btn("Switch to detected project"); + const assignBtn = btn("Assign to active project"); + const createBtn = btn("Create new project"); + const dismissBtn = btn("Dismiss"); + for (const b of [switchBtn, assignBtn, createBtn, dismissBtn]) { + b.style.fontSize = "11px"; + b.style.padding = "6px 8px"; + b.style.margin = "0"; + } + resolveBtns.appendChild(switchBtn); + resolveBtns.appendChild(assignBtn); + resolveBtns.appendChild(createBtn); + resolveBtns.appendChild(dismissBtn); + + resolveWrap.appendChild(resolveTitle); + resolveWrap.appendChild(resolveInfo); + resolveWrap.appendChild(resolveActive); + resolveWrap.appendChild(resolveBtns); + + const workflowWrap = document.createElement("div"); + workflowWrap.style.display = "none"; + workflowWrap.style.flexDirection = "column"; + workflowWrap.style.gap = UI_GAP; + workflowWrap.style.padding = "8px 10px"; + workflowWrap.style.borderRadius = "8px"; + workflowWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowWrap.style.backdropFilter = "blur(6px)"; + + const workflowTitle = document.createElement("div"); + workflowTitle.textContent = "Workflow Project"; + workflowTitle.style.textTransform = "uppercase"; + workflowTitle.style.letterSpacing = "0.6px"; + workflowTitle.style.fontWeight = "600"; + workflowTitle.style.color = "#8fb9ff"; + workflowTitle.style.marginBottom = "0"; + + const pendingProjectNameInput = document.createElement("input"); + pendingProjectNameInput.type = "text"; + pendingProjectNameInput.placeholder = "Project name"; + pendingProjectNameInput.value = state.pendingProjectName || ""; + + const pendingSelect = document.createElement("select"); + pendingSelect.className = "mjr-ps-select"; + pendingSelect.style.width = "100%"; + pendingSelect.style.padding = "6px 8px"; + pendingSelect.style.borderRadius = "8px"; + pendingSelect.style.border = "1px solid rgba(255,255,255,0.12)"; + pendingSelect.style.color = "#f2f4f8"; + + const wfCreateBtn = btn("Create & Activate"); + const wfActivateBtn = btn("Activate Selected"); + const wfAssignBtn = btn("Assign workflow to active"); + for (const b of [wfCreateBtn, wfActivateBtn, wfAssignBtn]) { + b.style.fontSize = "11px"; + b.style.padding = "6px 8px"; + b.style.margin = "0"; + } + + const wfButtons = document.createElement("div"); + wfButtons.style.display = "flex"; + wfButtons.style.gap = UI_GAP; + wfButtons.style.flexWrap = "wrap"; + wfButtons.style.justifyContent = "center"; + wfButtons.style.alignItems = "center"; + wfButtons.style.marginTop = "0"; + wfButtons.appendChild(wfCreateBtn); + wfButtons.appendChild(wfActivateBtn); + wfButtons.appendChild(wfAssignBtn); + + workflowWrap.appendChild(workflowTitle); + workflowWrap.appendChild(row("Project name", pendingProjectNameInput)); + workflowWrap.appendChild(row("Or select existing project", pendingSelect)); + workflowWrap.appendChild(wfButtons); + + let refreshProjectsList = () => {}; + + const updateWorkflowBlock = () => { + const show = !!state.workflowPanelOpen; + workflowWrap.style.display = show ? "flex" : "none"; + if (show) { + refreshProjectsList(); + pendingProjectNameInput.value = state.pendingProjectName || ""; + if (state.pendingSelectProjectId) { + pendingSelect.value = state.pendingSelectProjectId; + } + } + }; + + const autoPatchNewWrap = document.createElement("label"); + autoPatchNewWrap.style.display = "flex"; + autoPatchNewWrap.style.alignItems = "center"; + autoPatchNewWrap.style.gap = UI_GAP; + autoPatchNewWrap.style.cursor = "pointer"; + const autoPatchNewCb = document.createElement("input"); + autoPatchNewCb.type = "checkbox"; + autoPatchNewCb.checked = state.autoPatchNewNodes !== false; + const autoPatchNewText = document.createElement("span"); + autoPatchNewText.textContent = "Auto patch new nodes"; + autoPatchNewText.style.opacity = "0.9"; + autoPatchNewWrap.appendChild(autoPatchNewCb); + autoPatchNewWrap.appendChild(autoPatchNewText); + + const autoSwitchWrap = document.createElement("label"); + autoSwitchWrap.style.display = "flex"; + autoSwitchWrap.style.alignItems = "center"; + autoSwitchWrap.style.gap = UI_GAP; + autoSwitchWrap.style.cursor = "pointer"; + const autoSwitchCb = document.createElement("input"); + autoSwitchCb.type = "checkbox"; + autoSwitchCb.checked = state.autoSwitchTrusted === true; + const autoSwitchText = document.createElement("span"); + autoSwitchText.textContent = "Auto-switch trusted signature"; + autoSwitchText.style.opacity = "0.9"; + autoSwitchWrap.appendChild(autoSwitchCb); + autoSwitchWrap.appendChild(autoSwitchText); + + // Model section (collapsible) + const modelSection = detailsSection("Model"); + const modelSummary = modelSection._summary; + + const detectedModelLabel = document.createElement("div"); + detectedModelLabel.style.opacity = "0.9"; + detectedModelLabel.style.fontSize = "11px"; + detectedModelLabel.style.color = "#f2d98a"; + detectedModelLabel.style.textAlign = "left"; + detectedModelLabel.style.marginLeft = "2px"; + detectedModelLabel.style.marginTop = "0"; + detectedModelLabel.textContent = "Detected: -"; + + const useCustomWrap = document.createElement("label"); + useCustomWrap.style.display = "flex"; + useCustomWrap.style.alignItems = "center"; + useCustomWrap.style.gap = UI_GAP; + useCustomWrap.style.cursor = "pointer"; + const useCustomCb = document.createElement("input"); + useCustomCb.type = "checkbox"; + useCustomCb.checked = state.useCustomModel === true; + const useCustomText = document.createElement("span"); + useCustomText.textContent = "Use custom model (UPPERCASE)"; + useCustomText.style.opacity = "0.9"; + useCustomWrap.appendChild(useCustomCb); + useCustomWrap.appendChild(useCustomText); + + const modelSelect = document.createElement("select"); + modelSelect.className = "mjr-ps-select"; + modelSelect.style.width = "100%"; + modelSelect.style.padding = "6px 8px"; + modelSelect.style.borderRadius = "8px"; + modelSelect.style.border = "1px solid rgba(255,255,255,0.12)"; + modelSelect.style.color = "#f2f4f8"; + + const modelRow = document.createElement("div"); + modelRow.style.display = "flex"; + modelRow.style.gap = UI_GAP; + const modelToggleBtn = btn(state.showMoreModels ? "Show less" : "Show more"); + modelToggleBtn.style.padding = "4px 6px"; + modelToggleBtn.style.fontSize = "10px"; + modelToggleBtn.style.flex = "0 0 auto"; + modelToggleBtn.style.margin = "0"; + modelToggleBtn.style.alignSelf = "flex-end"; + const modelSelectWrap = row("Default Model", modelSelect); + modelSelectWrap.style.flex = "1 1 auto"; + modelRow.appendChild(modelSelectWrap); + + const modelEmptyMsg = document.createElement("div"); + modelEmptyMsg.style.opacity = "0.8"; + modelEmptyMsg.style.display = "none"; + modelEmptyMsg.style.textAlign = "center"; + modelEmptyMsg.style.fontSize = "11px"; + modelEmptyMsg.style.color = "#f2d98a"; + modelEmptyMsg.textContent = "No models detected (check folders)"; + + const customModelInput = document.createElement("input"); + customModelInput.type = "text"; + customModelInput.placeholder = "Custom model name"; + customModelInput.value = state.customModelText || ""; + + const customModelWrap = row("Custom Model (UPPERCASE)", customModelInput); + customModelWrap.style.display = state.useCustomModel ? "block" : "none"; + customModelWrap.style.flex = "1 1 auto"; + + const customModelPreview = document.createElement("div"); + customModelPreview.style.opacity = "0.85"; + customModelPreview.style.fontSize = "9px"; + customModelPreview.style.color = "#f2d98a"; + customModelPreview.style.textAlign = "center"; + customModelPreview.textContent = "Custom tag: -"; + customModelWrap.appendChild(customModelPreview); + + const modelCustomRow = document.createElement("div"); + modelCustomRow.style.display = "flex"; + modelCustomRow.style.gap = UI_GAP; + modelCustomRow.style.alignItems = "flex-end"; + modelCustomRow.appendChild(customModelWrap); + modelCustomRow.appendChild(modelToggleBtn); + + modelSection.appendChild(detectedModelLabel); + modelSection.appendChild(useCustomWrap); + modelSection.appendChild(modelRow); + modelSection.appendChild(modelCustomRow); + modelSection.appendChild(modelEmptyMsg); + + // Workflow section + const workflowNameInput = document.createElement("input"); + workflowNameInput.type = "text"; + workflowNameInput.placeholder = "Workflow name (optional)"; + workflowNameInput.value = state.workflowName || ""; + + const workflowAssetInput = document.createElement("input"); + workflowAssetInput.type = "text"; + workflowAssetInput.placeholder = "Asset folder (optional)"; + workflowAssetInput.value = state.workflowAsset || ""; + + const workflowPreviewLabel = document.createElement("div"); + workflowPreviewLabel.style.opacity = "0.85"; + workflowPreviewLabel.style.textAlign = "left"; + workflowPreviewLabel.style.fontSize = "9px"; + workflowPreviewLabel.style.color = "#f2d98a"; + workflowPreviewLabel.style.lineHeight = "1.35"; + workflowPreviewLabel.style.wordBreak = "break-word"; + workflowPreviewLabel.textContent = "Next save: -"; + + const saveWorkflowBtn = btn("Save Workflow to Project"); + saveWorkflowBtn.style.alignSelf = "center"; + saveWorkflowBtn.style.margin = "0 auto"; + saveWorkflowBtn.style.display = "block"; + + const autoHookWrap = document.createElement("label"); + autoHookWrap.style.display = "flex"; + autoHookWrap.style.alignItems = "center"; + autoHookWrap.style.gap = UI_GAP; + autoHookWrap.style.cursor = "pointer"; + const autoHookCb = document.createElement("input"); + autoHookCb.type = "checkbox"; + autoHookCb.checked = state.autoHookSave !== false; + const autoHookText = document.createElement("span"); + autoHookText.textContent = "Auto-hook Ctrl+S"; + autoHookText.style.opacity = "0.9"; + autoHookWrap.appendChild(autoHookCb); + autoHookWrap.appendChild(autoHookText); + + // Custom output section (collapsible) + const customOutputSection = detailsSection("Custom Output (optional)"); + customOutputSection.style.display = "flex"; + customOutputSection.style.flexDirection = "column"; + customOutputSection.style.gap = UI_GAP; + + const kindSelect = document.createElement("select"); + kindSelect.className = "mjr-ps-select"; + for (const [value, label] of [ + ["asset", "Asset"], + ["shot", "Shot"], + ]) { + const opt = document.createElement("option"); + opt.value = value; + opt.textContent = label; + kindSelect.appendChild(opt); + } + kindSelect.value = state.kind || "asset"; + + const mediaSelect = document.createElement("select"); + mediaSelect.className = "mjr-ps-select"; + for (const [value, label] of [ + ["images", "Images"], + ["videos", "Videos"], + ]) { + const opt = document.createElement("option"); + opt.value = value; + opt.textContent = label; + mediaSelect.appendChild(opt); + } + mediaSelect.value = state.media || "images"; + + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.placeholder = "Asset/shot name"; + nameInput.value = state.name || ""; + styleField(nameInput); + + const existingNamesSelect = document.createElement("select"); + existingNamesSelect.className = "mjr-ps-select"; + existingNamesSelect.style.display = "none"; + styleField(existingNamesSelect); + + const nameStack = document.createElement("div"); + nameStack.style.display = "flex"; + nameStack.style.flexDirection = "column"; + nameStack.style.gap = UI_GAP; + nameStack.style.width = "100%"; + nameStack.appendChild(existingNamesSelect); + nameStack.appendChild(nameInput); + + const autoPatchWrap = document.createElement("label"); + autoPatchWrap.style.display = "flex"; + autoPatchWrap.style.alignItems = "center"; + autoPatchWrap.style.gap = UI_GAP; + autoPatchWrap.style.cursor = "pointer"; + const autoPatchCb = document.createElement("input"); + autoPatchCb.type = "checkbox"; + autoPatchCb.checked = state.autoPatch !== false; + const autoPatchText = document.createElement("span"); + autoPatchText.textContent = "Auto patch outputs"; + autoPatchText.style.opacity = "0.9"; + autoPatchWrap.appendChild(autoPatchCb); + autoPatchWrap.appendChild(autoPatchText); + + const createApplyBtn = btn("Create + Apply"); + const patchNowBtn = btn("Patch now"); + + const buttonRow = document.createElement("div"); + buttonRow.style.display = "flex"; + buttonRow.style.gap = UI_GAP; + buttonRow.style.justifyContent = "center"; + buttonRow.style.alignItems = "center"; + buttonRow.style.marginTop = "0"; + buttonRow.style.marginBottom = "0"; + buttonRow.appendChild(createApplyBtn); + buttonRow.appendChild(patchNowBtn); + + const targetLabel = document.createElement("div"); + targetLabel.style.opacity = "0.85"; + targetLabel.style.textAlign = "left"; + targetLabel.style.fontSize = "9px"; + targetLabel.style.color = "#f2d98a"; + targetLabel.style.lineHeight = "1.35"; + targetLabel.style.wordBreak = "break-word"; + + const updateTargetLabel = () => { + if (state.lastRelDir) { + targetLabel.textContent = `Target: ${state.lastRelDir} | Prefix: ${state.lastPrefix}`; + } else { + targetLabel.textContent = "Target: none (create an output)"; + } + }; + + const rollOutputDateIfNeeded = () => { + const today = yymmddJS(); + const prefix = String(state.lastPrefix || ""); + const m = prefix.match(/^(\d{6})_/); + const oldDate = + (m ? m[1] : "") || + String(state.lastRelDir || "").replace(/\\/g, "/").match(/(?:^|\/)(\d{6})(?:\/|$)/)?.[1] || + ""; + + if (!oldDate || oldDate === today) return false; + + if (String(state.lastRelDir || "")) { + const rel = String(state.lastRelDir || "").replace(/\\/g, "/"); + const re = new RegExp(`(^|/)${oldDate}(/|$)`, "g"); + state.lastRelDir = rel.replace(re, `$1${today}$2`); + } + + if (prefix.startsWith(`${oldDate}_`)) { + state.lastPrefix = `${today}${prefix.slice(6)}`; + } + + saveState(state); + updateTargetLabel(); + return true; + }; + + rollOutputDateIfNeeded(); + updateTargetLabel(); + + // Advanced template + const advanced = document.createElement("details"); + advanced.style.marginTop = "0"; + advanced.style.display = "flex"; + advanced.style.flexDirection = "column"; + advanced.style.gap = UI_GAP; + const summary = document.createElement("summary"); + summary.textContent = "Advanced"; + summary.style.cursor = "pointer"; + summary.style.opacity = "0.85"; + summary.style.textTransform = "uppercase"; + summary.style.letterSpacing = "0.6px"; + summary.style.color = "#8fb9ff"; + + const templateInput = document.createElement("input"); + templateInput.type = "text"; + templateInput.placeholder = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + templateInput.value = + state.pathTemplate || "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + + const templateTokens = document.createElement("div"); + templateTokens.style.opacity = "0.85"; + templateTokens.style.fontSize = "8px"; + templateTokens.style.color = "#f2d98a"; + templateTokens.style.textAlign = "left"; + templateTokens.style.lineHeight = "1.35"; + templateTokens.style.wordBreak = "break-word"; + templateTokens.textContent = `Tokens: ${TEMPLATE_TOKENS.join(" ")}`; + + const previewLabel = document.createElement("div"); + previewLabel.style.opacity = "0.85"; + previewLabel.style.fontSize = "8px"; + previewLabel.style.color = "#f2d98a"; + previewLabel.style.textAlign = "left"; + previewLabel.style.lineHeight = "1.35"; + previewLabel.style.wordBreak = "break-word"; + previewLabel.textContent = "Preview: -"; + + advanced.appendChild(summary); + advanced.appendChild(row("Path Template", templateInput)); + advanced.appendChild(templateTokens); + advanced.appendChild(previewLabel); + + const workflowSaveWrap = detailsSection("Workflow"); + workflowSaveWrap.open = true; + workflowSaveWrap.style.padding = "8px 10px"; + workflowSaveWrap.style.borderRadius = "8px"; + workflowSaveWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowSaveWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowSaveWrap.style.backdropFilter = "blur(6px)"; + const workflowSaveContent = document.createElement("div"); + workflowSaveContent.style.display = "flex"; + workflowSaveContent.style.flexDirection = "column"; + workflowSaveContent.style.gap = UI_GAP; + workflowSaveContent.appendChild(row("Workflow name", workflowNameInput)); + workflowSaveContent.appendChild(row("Asset folder", workflowAssetInput)); + workflowSaveContent.appendChild(workflowPreviewLabel); + workflowSaveContent.appendChild(autoHookWrap); + workflowSaveContent.appendChild(saveWorkflowBtn); + workflowSaveWrap.appendChild(workflowSaveContent); + + const workflowUtilsWrap = detailsSection("Workflow Utilities"); + workflowUtilsWrap.open = true; + workflowUtilsWrap.style.padding = "8px 10px"; + workflowUtilsWrap.style.borderRadius = "8px"; + workflowUtilsWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + workflowUtilsWrap.style.background = "rgba(15, 17, 22, 0.75)"; + workflowUtilsWrap.style.backdropFilter = "blur(6px)"; + + const workflowUtilsContent = document.createElement("div"); + workflowUtilsContent.style.display = "flex"; + workflowUtilsContent.style.flexDirection = "column"; + workflowUtilsContent.style.gap = UI_GAP; + + const fixMissingBtn = btn("Fix Missing Models"); + fixMissingBtn.style.alignSelf = "flex-start"; + + const downloadMissingBtn = btn("Download Missing Models"); + downloadMissingBtn.style.alignSelf = "flex-start"; + + const utilsButtonRow = document.createElement("div"); + utilsButtonRow.style.display = "flex"; + utilsButtonRow.style.gap = UI_GAP; + utilsButtonRow.style.justifyContent = "flex-start"; + utilsButtonRow.style.alignItems = "center"; + utilsButtonRow.style.flexWrap = "wrap"; + utilsButtonRow.appendChild(fixMissingBtn); + utilsButtonRow.appendChild(downloadMissingBtn); + + const fixerStatus = document.createElement("div"); + fixerStatus.style.opacity = "0.85"; + fixerStatus.style.fontSize = "9px"; + fixerStatus.style.color = "#f2d98a"; + fixerStatus.style.lineHeight = "1.35"; + fixerStatus.style.marginLeft = "2px"; + + const downloadStatus = document.createElement("div"); + downloadStatus.style.display = "flex"; + downloadStatus.style.flexDirection = "column"; + downloadStatus.style.gap = "4px"; + downloadStatus.style.opacity = "0.85"; + downloadStatus.style.fontSize = "9px"; + downloadStatus.style.color = "#f2d98a"; + downloadStatus.style.lineHeight = "1.35"; + downloadStatus.style.marginLeft = "2px"; + + const downloadStatusText = document.createElement("div"); + downloadStatus.appendChild(downloadStatusText); + + const downloadProgressWrap = document.createElement("div"); + downloadProgressWrap.style.height = "6px"; + downloadProgressWrap.style.width = "100%"; + downloadProgressWrap.style.borderRadius = "999px"; + downloadProgressWrap.style.border = "1px solid rgba(255,255,255,0.12)"; + downloadProgressWrap.style.background = "rgba(0,0,0,0.25)"; + downloadProgressWrap.style.overflow = "hidden"; + + const downloadProgressBar = document.createElement("div"); + downloadProgressBar.style.height = "100%"; + downloadProgressBar.style.width = "0%"; + downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; + downloadProgressBar.style.transition = "width 0.2s ease"; + downloadProgressWrap.appendChild(downloadProgressBar); + downloadStatus.appendChild(downloadProgressWrap); + + const fingerprintDetails = detailsSection("Advanced (Fingerprint)"); + fingerprintDetails.style.marginTop = "0"; + + const fingerprintContent = document.createElement("div"); + fingerprintContent.style.display = "flex"; + fingerprintContent.style.flexDirection = "column"; + fingerprintContent.style.gap = UI_GAP; + + const buildFingerprintBtn = btn("Build Model Fingerprint Cache"); + buildFingerprintBtn.style.alignSelf = "flex-start"; + + fingerprintContent.appendChild(buildFingerprintBtn); + fingerprintDetails.appendChild(fingerprintContent); + + workflowUtilsContent.appendChild(utilsButtonRow); + workflowUtilsContent.appendChild(fixerStatus); + workflowUtilsContent.appendChild(downloadStatus); + workflowUtilsContent.appendChild(fingerprintDetails); + workflowUtilsWrap.appendChild(workflowUtilsContent); + + container.appendChild(statusBar); + container.appendChild(workflowWrap); + container.appendChild(resolveWrap); + container.appendChild(divider()); + container.appendChild(modelSection); + + const registrySection = detailsSection("Community Registry"); + registrySection.style.display = "flex"; + registrySection.style.flexDirection = "column"; + registrySection.style.gap = "10px"; + const registryInput = document.createElement("input"); + registryInput.placeholder = "search model name or alias"; + registryInput.style.width = "100%"; + registryInput.style.padding = "6px 8px"; + registryInput.style.borderRadius = "8px"; + registryInput.style.border = "1px solid rgba(255,255,255,0.15)"; + registryInput.style.background = "rgba(255,255,255,0.04)"; + registryInput.style.color = "#f4f6fb"; + registryInput.style.fontSize = "13px"; + + const registryActions = document.createElement("div"); + registryActions.style.display = "flex"; + registryActions.style.gap = "8px"; + registryActions.style.flexWrap = "wrap"; + + const registrySearchBtn = btn("Search registry"); + const registryContributeBtn = btn("Contribute URL"); + registryActions.appendChild(registrySearchBtn); + registryActions.appendChild(registryContributeBtn); + + const registryStatus = document.createElement("div"); + registryStatus.style.fontSize = "12px"; + registryStatus.style.display = "block"; + registryStatus.style.minHeight = "18px"; + registryStatus.style.opacity = "0.8"; + + const registryResultsList = document.createElement("div"); + registryResultsList.style.display = "flex"; + registryResultsList.style.flexDirection = "column"; + registryResultsList.style.gap = "10px"; + + const renderRegistryResults = (results) => { + registryResultsList.innerHTML = ""; + if (!results || results.length === 0) { + registryResultsList.textContent = "No registry matches yet."; + return; + } + for (const item of results) { + const card = document.createElement("div"); + card.style.border = "1px solid rgba(255,255,255,0.15)"; + card.style.borderRadius = "10px"; + card.style.padding = "10px"; + card.style.display = "flex"; + card.style.flexDirection = "column"; + card.style.gap = "4px"; + card.style.background = "rgba(255,255,255,0.02)"; + + const titleRow = document.createElement("div"); + titleRow.style.display = "flex"; + titleRow.style.justifyContent = "space-between"; + titleRow.style.alignItems = "baseline"; + + const title = document.createElement("strong"); + title.textContent = item.name || "Unnamed model"; + title.style.fontSize = "14px"; + title.style.wordBreak = "break-word"; + + const score = document.createElement("span"); + score.textContent = `${Math.round(item.match_score || 0)}%`; + score.style.color = "#a9ff8f"; + score.style.fontSize = "12px"; + titleRow.appendChild(title); + titleRow.appendChild(score); + + const meta = document.createElement("div"); + meta.style.fontSize = "11px"; + meta.style.opacity = "0.8"; + meta.textContent = `${item.platform || "community"} · ${item.type || "checkpoint"}`; + + const footer = document.createElement("div"); + footer.style.display = "flex"; + footer.style.gap = "8px"; + + if (item.url) { + const openBtn = document.createElement("button"); + openBtn.textContent = "Open"; + openBtn.className = "mjr-ps-btn"; + openBtn.style.flex = "1"; + openBtn.addEventListener("click", () => window.open(item.url, "_blank")); + footer.appendChild(openBtn); + } + + const copyBtn = document.createElement("button"); + copyBtn.textContent = "Copy URL"; + copyBtn.className = "mjr-ps-btn"; + copyBtn.style.flex = "1"; + copyBtn.addEventListener("click", async () => { + if (item.url) { + await navigator.clipboard.writeText(item.url); + toast("success", "Copied", "URL added to clipboard."); + } + }); + footer.appendChild(copyBtn); + + card.appendChild(titleRow); + card.appendChild(meta); + card.appendChild(footer); + registryResultsList.appendChild(card); + } + }; + + renderRegistryResults([]); + + const triggerRegistrySearch = async () => { + const query = registryInput.value.trim(); + if (!query) { + registryStatus.textContent = "Enter a query to search the registry."; + return; + } + registryStatus.textContent = "Searching registry..."; + registrySearchBtn.disabled = true; + try { + const resp = await searchRegistry(query, 6); + const results = resp?.results || []; + registryStatus.textContent = `Found ${results.length} result${results.length !== 1 ? "s" : ""}.`; + renderRegistryResults(results); + } catch (error) { + console.error("[registry search]", error); + registryStatus.textContent = "Registry search failed."; + } finally { + registrySearchBtn.disabled = false; + } + }; + + registrySearchBtn.addEventListener("click", triggerRegistrySearch); + registryInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + triggerRegistrySearch(); + } + }); + registryContributeBtn.addEventListener("click", () => showModelContributionDialog()); + + registrySection.appendChild(registryInput); + registrySection.appendChild(registryActions); + registrySection.appendChild(registryStatus); + registrySection.appendChild(registryResultsList); + container.appendChild(registrySection); + + container.appendChild(autoPatchNewWrap); + container.appendChild(autoSwitchWrap); + container.appendChild(divider()); + container.appendChild(workflowSaveWrap); + container.appendChild(workflowUtilsWrap); + container.appendChild(divider()); + customOutputSection.appendChild(row("Kind", kindSelect)); + customOutputSection.appendChild(row("Media", mediaSelect)); + customOutputSection.appendChild(groupRow("Name", nameStack)); + customOutputSection.appendChild(autoPatchWrap); + customOutputSection.appendChild(buttonRow); + customOutputSection.appendChild(targetLabel); + container.appendChild(customOutputSection); + container.appendChild(divider()); + container.appendChild(advanced); + + el.appendChild(container); + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + + const fixerState = { + missing: 0, + fixed: 0, + total: 0, + cacheCount: 0, + cacheUpdatedAt: "", + cacheError: "", + scanned: false, + lastScanToken: -1, + }; + + const downloadState = { + state: "idle", + message: "", + progress: { current: 0, total: 0, pct: 0 }, + }; + + const updateFixerStatus = () => { + const cacheText = fixerState.cacheError + ? "error" + : `${fixerState.cacheCount} / ${fixerState.cacheUpdatedAt || "-"}`; + const missingText = fixerState.scanned ? fixerState.missing : "-"; + const fixedText = fixerState.scanned ? fixerState.fixed : "-"; + fixerStatus.textContent = `Missing: ${missingText} | Fixed: ${fixedText} | Cache: ${cacheText}`; + + const total = fixerState.total || 0; + let color = "#666666"; + if (fixerState.scanned) { + if (total > 0 && fixerState.missing === 0) { + color = "#2f8f4e"; + } else if (total > 0 && fixerState.missing > 0 && fixerState.missing < total) { + color = "#c77c2a"; + } else if (total > 0 && fixerState.missing >= total) { + color = "#b33a3a"; + } + } + workflowUtilsWrap.style.border = `1px solid ${colorWithAlpha(color, 0.4)}`; + workflowUtilsWrap.style.background = colorWithAlpha(color, 0.12); + }; + + const updateDownloadStatus = () => { + const rawState = downloadState.state || "idle"; + const stateLabel = rawState === "queued" ? "downloading" : rawState; + const pct = downloadState.progress?.pct || 0; + const pctText = pct ? ` ${pct}%` : ""; + const msg = downloadState.message ? ` | ${downloadState.message}` : ""; + downloadStatusText.textContent = `Download status: ${stateLabel}${pctText}${msg}`; + + let barPct = Math.max(0, Math.min(100, Number(pct) || 0)); + if (rawState === "done" && barPct === 0) { + barPct = 100; + } + if (rawState === "downloading" && barPct === 0 && downloadState.progress?.current) { + barPct = 5; + } + downloadProgressBar.style.width = `${barPct}%`; + + if (rawState === "error") { + downloadProgressBar.style.background = "rgba(179, 58, 58, 0.7)"; + } else if (rawState === "done") { + downloadProgressBar.style.background = "rgba(47, 143, 78, 0.7)"; + } else if (rawState === "idle") { + downloadProgressBar.style.background = "rgba(255, 255, 255, 0.2)"; + } else { + downloadProgressBar.style.background = "rgba(46, 136, 255, 0.7)"; + } + }; + + const refreshFingerprintStatus = async () => { + try { + const resp = await getFingerprintCacheStatus(); + fixerState.cacheCount = Number(resp?.count || 0); + fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); + fixerState.cacheError = ""; + } catch (e) { + fixerState.cacheError = "error"; + } + updateFixerStatus(); + }; + + updateFixerStatus(); + updateDownloadStatus(); + + let missingScanTimer = null; + const refreshMissingStatus = (force = false) => { + const autoScan = getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true); + if (!force && !autoScan) { + updateFixerStatus(); + return; + } + + const scanToken = Number(state.graphLoadToken || 0); + if (!force && fixerState.scanned && fixerState.lastScanToken === scanToken) { + updateFixerStatus(); + return; + } + + if (missingScanTimer) { + clearTimeout(missingScanTimer); + missingScanTimer = null; + } + + missingScanTimer = setTimeout(() => { + try { + const stats = scanMissingModelsWithStats(); + fixerState.missing = stats.missing.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = scanToken; + if (fixerState.missing === 0) { + fixerState.fixed = 0; + } + } finally { + updateFixerStatus(); + } + }, 0); + }; + + const isEditableTarget = (target) => { + if (!target) return false; + const tag = String(target.tagName || "").toLowerCase(); + return target.isContentEditable || tag === "input" || tag === "textarea" || tag === "select"; + }; + + container.addEventListener("keydown", (event) => { + const key = String(event.key || "").toLowerCase(); + if (event.altKey && !event.ctrlKey && !event.metaKey) { + if (key === "a") { + event.preventDefault(); + createApplyBtn.click(); + } else if (key === "p") { + event.preventDefault(); + patchNowBtn.click(); + } + } + }); + + const resolveModel = () => { + if (useCustomCb.checked) { + const tag = token3Tag(customModelInput.value, true); + return { + modelRaw: customModelInput.value || "Unknown", + modelUpper: true, + tag: tag || "UNKNOWN", + }; + } + if (state.detectedModelTag) { + return { + modelRaw: state.detectedModelTag, + modelUpper: false, + tag: state.detectedModelTag, + }; + } + const sel = modelSelect.value || "Unknown"; + const tag = token3Tag(sel, false); + return { modelRaw: sel, modelUpper: false, tag: tag || "Unknown" }; + }; + + const buildWorkflowName = () => { + const modelInfo = resolveModel(); + const modelTag = modelInfo.tag || "Model"; + const assetName = titlePathJS(nameInput.value || "Asset"); + return `${yymmddJS()}_${modelTag}_${assetName}`; + }; + + const buildWorkflowPreview = () => { + const base = buildWorkflowName(); + const next = + base && state.workflowLastBase === base + ? Number(state.workflowNextSuffix || 1) + : 1; + const suffix = String(next).padStart(4, "0"); + return `${base}_${suffix}.json`; + }; + + let existingNamesToken = 0; + let lastExistingKey = ""; + + const refreshExistingNames = async (force = false) => { + const projectId = state.projectId || ""; + const media = mediaSelect.value || "images"; + const key = `${projectId}::${media}`; + if (!force && key === lastExistingKey) return; + lastExistingKey = key; + + existingNamesSelect.innerHTML = ""; + const emptyOpt = document.createElement("option"); + emptyOpt.value = ""; + emptyOpt.textContent = "-- Select existing name --"; + existingNamesSelect.appendChild(emptyOpt); + + if (!projectId) { + existingNamesSelect.style.display = "none"; + return; + } + + const requestToken = (existingNamesToken += 1); + try { + const names = await listExistingNames(projectId, media); + if (requestToken !== existingNamesToken) return; + if (!names.length) { + existingNamesSelect.style.display = "none"; + return; + } + for (const name of names) { + const opt = document.createElement("option"); + opt.value = name; + opt.textContent = name; + existingNamesSelect.appendChild(opt); + } + if (nameInput.value && names.includes(nameInput.value)) { + existingNamesSelect.value = nameInput.value; + } else { + existingNamesSelect.value = ""; + } + existingNamesSelect.style.display = "block"; + } catch (_) { + if (requestToken !== existingNamesToken) return; + existingNamesSelect.style.display = "none"; + } + }; + + let previewToken = 0; + + const updatePreview = async () => { + const baseRel = state.projectFolder + ? `PROJECTS/${state.projectFolder}` + : "PROJECTS/"; + const modelInfo = resolveModel(); + const tokens = { + BASE: baseRel, + MEDIA: mediaDir(mediaSelect.value), + DATE: yymmddJS(), + MODEL: String(modelInfo.tag || ""), + NAME: titlePathJS(nameInput.value || "Name"), + KIND: makeKindToken(kindSelect.value), + }; + const template = String(templateInput.value || "").trim(); + const requestToken = (previewToken += 1); + + detectedModelLabel.textContent = `Detected: ${state.detectedModelTag || "-"}`; + customModelPreview.textContent = useCustomCb.checked + ? `Custom tag: ${modelInfo.tag}` + : "Custom tag: -"; + if (modelSummary) { + const activeTag = modelInfo.tag || "-"; + modelSummary.textContent = `MODEL (${activeTag})`; + } + const assetFolder = titlePathJS(workflowAssetInput.value || ""); + const workflowFile = buildWorkflowPreview(); + const workflowRel = assetFolder + ? `03_WORKFLOWS/${assetFolder}/${workflowFile}` + : `03_WORKFLOWS/${workflowFile}`; + workflowPreviewLabel.textContent = `Next save: ${workflowRel}`; + modelSelect.disabled = !!state.detectedModelTag && !useCustomCb.checked; + modelSelect.style.opacity = modelSelect.disabled ? "0.6" : "1"; + + if (!template) { + previewLabel.textContent = "Preview: -"; + return; + } + + previewLabel.textContent = "Preview: ..."; + try { + const resp = await previewTemplate(template, tokens); + if (requestToken !== previewToken) return; + const preview = resp?.preview || ""; + previewLabel.textContent = preview ? `Preview: ${preview}` : "Preview: -"; + } catch (err) { + if (requestToken !== previewToken) return; + const previewError = String(err?.message || err || "Invalid template"); + previewLabel.textContent = `Preview: ${previewError}`; + } + }; + + const updateState = () => { + state.workflowName = workflowNameInput.value; + state.workflowAsset = workflowAssetInput.value; + state.kind = kindSelect.value; + state.media = mediaSelect.value; + state.name = nameInput.value; + state.customModelText = customModelInput.value; + state.modelSelection = modelSelect.value; + state.autoPatch = autoPatchCb.checked; + state.autoPatchNewNodes = autoPatchNewCb.checked; + state.autoSwitchTrusted = autoSwitchCb.checked; + state.autoHookSave = autoHookCb.checked; + state.useCustomModel = useCustomCb.checked; + state.modelUpper = useCustomCb.checked; + state.pendingProjectName = pendingProjectNameInput.value; + state.pendingSelectProjectId = pendingSelect.value; + state.pathTemplate = templateInput.value; + customModelWrap.style.display = useCustomCb.checked ? "block" : "none"; + saveState(state); + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + updatePreview(); + if (!state.lastRelDir) { + targetLabel.textContent = "Target: none (create an output)"; + } + }; + + const setWorkflowName = (value) => { + workflowNameInput.value = value || ""; + state.workflowName = workflowNameInput.value; + saveState(state); + updatePreview(); + }; + + const resetForProjectChange = () => { + state.projectName = ""; + state.workflowName = ""; + state.workflowAsset = ""; + state.workflowLastBase = ""; + state.workflowNextSuffix = 1; + state.workflowPanelOpen = false; + state.name = ""; + state.kind = "asset"; + state.media = "images"; + state.modelSelection = ""; + state.useCustomModel = false; + state.customModelText = ""; + state.modelUpper = false; + state.lastRelDir = ""; + state.lastPrefix = ""; + state.pathTemplate = "{BASE}/{MEDIA}/{DATE}/{NAME}/{MODEL}"; + + workflowNameInput.value = ""; + workflowAssetInput.value = ""; + nameInput.value = ""; + kindSelect.value = "asset"; + mediaSelect.value = "images"; + if (modelSelect.options.length > 0) { + modelSelect.selectedIndex = 0; + } + useCustomCb.checked = false; + customModelInput.value = ""; + customModelWrap.style.display = "none"; + templateInput.value = state.pathTemplate; + targetLabel.textContent = "Target: none (create an output)"; + updateState(); + updatePreview(); + }; + + // Assign UI helper functions after all are defined + state._ui = { + updateStatus, + updateResolve, + updatePreview, + updateWorkflowBlock, + updateTargetLabel, + resetForProjectChange, + buildWorkflowName, + setWorkflowName, + refreshExistingNames, + refreshProjectsList: (force = false) => refreshProjectsList(force), + refreshMissingStatus, + }; + + // Store full project list for search filtering + let allProjects = []; + let lastProjectsRefreshAt = 0; + let projectRefreshErrorCount = 0; + + const updateProjectDropdowns = (filteredList) => { + pendingSelect.innerHTML = ""; + const pendingEmpty = document.createElement("option"); + pendingEmpty.value = ""; + pendingEmpty.textContent = "-"; + pendingSelect.appendChild(pendingEmpty); + + for (const p of filteredList) { + const opt = document.createElement("option"); + opt.value = p.project_id; + opt.dataset.folder = p.folder || ""; + opt.dataset.exists = p.exists === false ? "false" : "true"; + const missingTag = p.exists === false ? " (missing)" : ""; + const archivedTag = p.archived ? " [archived]" : ""; + opt.textContent = `${p.folder || p.project_id} (${p.project_id})${missingTag}${archivedTag}`; + pendingSelect.appendChild(opt); + } + }; + + refreshProjectsList = async (force = false) => { + const now = Date.now(); + + // Apply exponential backoff on errors + const throttleMs = projectRefreshErrorCount > 0 + ? PROJECT_REFRESH_ERROR_BACKOFF_MS * Math.min(projectRefreshErrorCount, 5) + : PROJECT_REFRESH_THROTTLE_MS; + + if (!force && now - lastProjectsRefreshAt < throttleMs) return; + + lastProjectsRefreshAt = now; + try { + const resp = await fetchJSON("/mjr_project/list"); + allProjects = resp.projects || []; + state.lastError = ""; + projectRefreshErrorCount = 0; // Reset error count on success + + updateProjectDropdowns(allProjects); + if (state.projectId) { + const active = allProjects.find((p) => p.project_id === state.projectId); + state.projectExists = active ? active.exists !== false : false; + } + if (state.pendingSelectProjectId) { + pendingSelect.value = state.pendingSelectProjectId; + } + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + } catch (e) { + projectRefreshErrorCount++; + const errorMsg = String(e.message || e); + state.lastError = errorMsg; + updateStatus(); + + // Only show toast on first error to avoid spam + if (projectRefreshErrorCount === 1) { + toast("error", "Projects list failed", errorMsg); + } + console.error("[mjr] Project list refresh failed:", e); + } + }; + + workflowNameInput.addEventListener("input", updateState); + workflowAssetInput.addEventListener("input", updateState); + kindSelect.addEventListener("change", updateState); + mediaSelect.addEventListener("change", () => { + updateState(); + refreshExistingNames(true); + }); + nameInput.addEventListener("input", updateState); + existingNamesSelect.addEventListener("change", () => { + nameInput.value = existingNamesSelect.value || ""; + updateState(); + }); + customModelInput.addEventListener("input", updateState); + useCustomCb.addEventListener("change", updateState); + autoPatchCb.addEventListener("change", updateState); + autoPatchNewCb.addEventListener("change", updateState); + autoSwitchCb.addEventListener("change", updateState); + autoHookCb.addEventListener("change", updateState); + templateInput.addEventListener("input", updateState); + pendingProjectNameInput.addEventListener("input", updateState); + pendingSelect.addEventListener("change", updateState); + + const runFixMissingModels = async () => { + debug("[MJR] Fix Missing Models clicked"); + const originalLabel = fixMissingBtn.textContent; + fixMissingBtn.disabled = true; + fixMissingBtn.textContent = "Scanning..."; + try { + debug("[MJR] Scanning for missing models..."); + const stats = scanMissingModelsWithStats(); + debug("[MJR] Scan results:", stats); + const missing = stats.missing; + fixerState.missing = missing.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = 0; + updateFixerStatus(); + + if (!missing.length) { + debug("[MJR] No missing models found"); + toast("info", "No missing models", "All model widgets match available options."); + return; + } + + const payload = missing.map((item) => ({ + missing_value: item.missing_value, + type_hint: item.type_hint, + })); + debug("[MJR] Sending scan request to API:", payload); + const resp = await scanModelCandidates(payload); + debug("[MJR] API response:", resp); + const results = Array.isArray(resp?.results) ? resp.results : []; + const resultMap = new Map(); + for (const entry of results) { + const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; + resultMap.set(key, Array.isArray(entry?.candidates) ? entry.candidates : []); + } + + const autoFixes = []; + const autoFixedKeys = new Set(); + for (const entry of missing) { + const key = `${String(entry?.missing_value || "")}::${String(entry?.type_hint || "unknown")}`; + const candidates = resultMap.get(key) || []; + const exact = candidates.filter( + (c) => Number(c?.score || 0) === 100 && !c?.in_wrong_folder && c?.reason !== "wrong_folder" + ); + if (exact.length === 1) { + const relpath = String(exact[0]?.relpath || ""); + if (relpath) { + autoFixes.push({ + node_id: entry?.node_id, + widget_name: entry?.widget_name, + new_value: relpath, + }); + autoFixedKeys.add(`${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`); + } + } + } + + let fixedCount = 0; + let remaining = missing.filter((entry) => { + const key = `${entry?.node_id ?? ""}::${entry?.widget_name ?? ""}::${entry?.missing_value ?? ""}`; + return !autoFixedKeys.has(key); + }); + + if (autoFixes.length) { + fixedCount = applyFixesToGraph(autoFixes); + const postStats = scanMissingModelsWithStats(); + remaining = postStats.missing; + fixerState.missing = remaining.length; + fixerState.total = postStats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = fixedCount; + updateFixerStatus(); + } + + if (!remaining.length) { + if (fixedCount > 0) { + toast("success", "Fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); + } else { + toast("info", "No missing models", "All model widgets match available options."); + } + return; + } + + const fixes = await showModelFixerDialog({ + missing: remaining, + results, + }); + debug("[MJR] User selected fixes:", fixes); + if (!fixes) { + if (fixedCount > 0) { + toast("success", "Auto-fix applied", `Updated ${fixedCount} widget${fixedCount !== 1 ? "s" : ""}.`); + } else { + toast("info", "Fix cancelled", "No changes applied."); + } + return; + } + const fixed = applyFixesToGraph(fixes); + debug("[MJR] Applied fixes count:", fixed); + const postStats = scanMissingModelsWithStats(); + fixerState.missing = postStats.missing.length; + fixerState.total = postStats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + fixerState.fixed = fixedCount + fixed; + updateFixerStatus(); + if (fixed + fixedCount > 0) { + toast( + "success", + "Fix applied", + `Updated ${fixed + fixedCount} widget${fixed + fixedCount !== 1 ? "s" : ""}.` + ); + } else { + toast("warn", "No changes", "No widgets were updated."); + } + } catch (e) { + console.error("[MJR] Fix missing models error:", e); + toast("error", "Fix failed", String(e?.message || e)); + } finally { + fixMissingBtn.textContent = originalLabel; + fixMissingBtn.disabled = false; + } + }; + + const uniqueMissingForDownload = (missing) => { + const seen = new Set(); + const out = []; + for (const item of missing || []) { + const key = `${item.missing_value}::${item.type_hint || ""}`; + if (seen.has(key)) continue; + seen.add(key); + out.push(item); + } + return out; + }; + + const basename = (value) => normalizeKey(value); + + const pollDownloadJob = async (jobId) => { + while (true) { + try { + const status = await getDownloadStatus(jobId); + downloadState.state = status?.state || "downloading"; + downloadState.message = status?.message || ""; + downloadState.progress = status?.progress || { current: 0, total: 0, pct: 0 }; + updateDownloadStatus(); + if (downloadState.state === "done" || downloadState.state === "error") { + return status; + } + } catch (e) { + downloadState.state = "error"; + downloadState.message = "status check failed"; + updateDownloadStatus(); + return null; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + }; + + const runDownloadMissingModels = async () => { + debug("[MJR] Download Missing Models clicked"); + if (downloadState.state === "downloading") { + debug("[MJR] Download already in progress"); + toast("warn", "Download in progress", "Please wait for the current job to finish."); + return; + } + const originalLabel = downloadMissingBtn.textContent; + downloadMissingBtn.disabled = true; + downloadMissingBtn.textContent = "Scanning..."; + downloadState.state = "idle"; + downloadState.message = "scanning"; + updateDownloadStatus(); + + try { + debug("[MJR] Scanning for missing models..."); + const stats = scanMissingModelsWithStats(); + debug("[MJR] Scan results:", stats); + const missingAll = stats.missing; + const missing = uniqueMissingForDownload(missingAll); + debug("[MJR] Unique missing for download:", missing); + fixerState.missing = missingAll.length; + fixerState.total = stats.total || 0; + fixerState.scanned = true; + fixerState.lastScanToken = Number(state.graphLoadToken || 0); + updateFixerStatus(); + if (!missing.length) { + debug("[MJR] No missing models found"); + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + toast("info", "No missing models", "All model widgets match available options."); + return; + } + + const payload = missing.map((item) => ({ + missing_value: item.missing_value, + type_hint: item.type_hint, + })); + const workflowJson = getSerializedWorkflow(); + const noteTexts = extractNoteTexts(workflowJson); + const noteRecipes = collectNoteRecipes(workflowJson); + const resolved = await resolveModelRecipes(payload); + const resolvedMap = new Map(); + for (const entry of resolved?.resolved || []) { + const key = basename(entry.key || entry.missing_value); + resolvedMap.set(key, entry); + } + + const kindOptions = getKindOptions(); + let existingMap = null; + try { + const modelResp = await getModels(); + const categories = modelResp?.categories || {}; + existingMap = new Map(); + for (const [kind, items] of Object.entries(categories)) { + if (!Array.isArray(items)) continue; + const set = new Set(); + for (const item of items) { + const raw = String(item || "").replace(/\\/g, "/").toLowerCase(); + if (!raw) continue; + set.add(raw); + const base = raw.split("/").pop(); + if (base) set.add(base); + } + existingMap.set(kind, set); + } + } catch (_) { + existingMap = null; + } + + // Helper to check if model exists + const modelExists = (kind, filename) => { + if (!existingMap || !kind || !filename) return false; + const set = existingMap.get(kind); + if (!set) return false; + const normalized = String(filename || "").replace(/\\/g, "/").toLowerCase(); + return set.has(normalized) || set.has(normalized.split("/").pop()); + }; + + let matches = 0; + const allEntries = missing.map((item) => { + const key = basename(item.missing_value); + const stored = resolvedMap.get(key) || {}; + const fallbackKind = stored.kind || typeHintToKind(item.type_hint); + const noteRecipe = noteRecipes.get(key); + let recipe = stored.recipe || null; + let prefilled = false; + if (noteRecipe) { + recipe = noteRecipe; + if (recipe?.kind === "unknown" && fallbackKind) { + recipe = { ...recipe, kind: fallbackKind }; + } + prefilled = true; + matches += 1; + } else if (recipe) { + recipe = { ...recipe, source: "stored" }; + prefilled = true; + } + + const kind = stored.kind || fallbackKind; + const filename = recipe?.filename || key; + const exists = modelExists(kind, filename); + + return { + missing_value: item.missing_value, + key, + kind, + recipe, + prefilled, + exists, // Flag to track if model already downloaded + }; + }); + + // Filter to only show truly missing models (not already downloaded) + const dialogEntries = allEntries.filter((entry) => !entry.exists); + const alreadyDownloaded = allEntries.length - dialogEntries.length; + const entryMap = new Map(dialogEntries.map((entry) => [entry.key, entry])); + + console.debug("[mjr] download filter", { + total: allEntries.length, + missing: dialogEntries.length, + alreadyDownloaded, + notes: noteTexts.length, + recipes: noteRecipes.size, + matches, + }); + + if (dialogEntries.length === 0) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + if (alreadyDownloaded > 0) { + toast("success", "All models downloaded", `${alreadyDownloaded} model${alreadyDownloaded !== 1 ? 's' : ''} already in models folder.`); + } else { + toast("info", "No downloads needed", "No models to download."); + } + return; + } + + const dialogResult = await showModelDownloaderDialog({ + entries: dialogEntries, + kindOptions, + existingMap, + }); + if (!dialogResult) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + return; + } + + const items = dialogResult.items || []; + if (!items.length) { + downloadState.state = "idle"; + downloadState.message = ""; + updateDownloadStatus(); + toast("warn", "No downloads", "Select at least one entry to download."); + return; + } + + const autoRememberNotes = getSettingBool(SETTING_AUTO_REMEMBER_NOTE_SOURCES, true); + const saveItems = []; + if (dialogResult.remember && dialogResult.saveItems?.length) { + saveItems.push(...dialogResult.saveItems); + } + if (autoRememberNotes) { + for (const item of items) { + const entry = entryMap.get(item.key); + if (entry?.recipe?.source === "workflow_note") { + saveItems.push(item); + } + } + } + if (saveItems.length) { + const seenKeys = new Set(); + const deduped = []; + for (const item of saveItems) { + if (!item || !item.key) continue; + if (seenKeys.has(item.key)) continue; + seenKeys.add(item.key); + deduped.push(item); + } + if (deduped.length) { + await saveModelRecipes(deduped); + } + } + + const resp = await downloadModels(items); + const jobId = resp?.job_id; + if (!jobId) { + throw new Error("download job not created"); + } + downloadState.state = "downloading"; + downloadState.message = "queued"; + downloadState.progress = { current: 0, total: 0, pct: 0 }; + updateDownloadStatus(); + + downloadMissingBtn.textContent = "Downloading..."; + const finalStatus = await pollDownloadJob(jobId); + const summary = finalStatus?.summary || {}; + const downloaded = Number(summary.downloaded || 0); + const errors = Number(summary.errors || 0); + const skipped = Number(summary.skipped || 0); + if (downloadState.state === "error") { + toast( + "error", + "Download finished", + `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}` + ); + } else { + toast( + "success", + "Download finished", + `Downloaded ${downloaded}, skipped ${skipped}, errors ${errors}` + ); + } + + try { + loadModels(); + } catch (_) {} + try { + app.graph?.setDirtyCanvas?.(true, true); + } catch (_) {} + + const autoFix = getSettingBool(SETTING_AUTO_FIX_AFTER_DOWNLOAD, true); + if (autoFix) { + await runFixMissingModels(); + } else { + const runFix = await psConfirm({ + title: "Run Fix Missing Models?", + message: "Downloads finished. Run Fix Missing Models now?", + }); + if (runFix) { + await runFixMissingModels(); + } else { + toast("info", "Fix skipped", "Re-open model dropdowns if they do not refresh."); + } + } + } catch (e) { + downloadState.state = "error"; + downloadState.message = "download failed"; + updateDownloadStatus(); + toast("error", "Download failed", String(e?.message || e)); + } finally { + downloadMissingBtn.textContent = originalLabel; + downloadMissingBtn.disabled = false; + } + }; + + fixMissingBtn.addEventListener("click", runFixMissingModels); + downloadMissingBtn.addEventListener("click", runDownloadMissingModels); + + buildFingerprintBtn.addEventListener("click", async () => { + const originalLabel = buildFingerprintBtn.textContent; + buildFingerprintBtn.disabled = true; + buildFingerprintBtn.textContent = "Building..."; + try { + const resp = await buildFingerprintCache({}); + const count = Number(resp?.count || 0); + fixerState.cacheCount = count; + fixerState.cacheUpdatedAt = String(resp?.updated_at || ""); + fixerState.cacheError = ""; + updateFixerStatus(); + const hashed = Number(resp?.hashed || 0); + const reused = Number(resp?.reused || 0); + toast("success", "Cache built", `Items: ${count} | Hashed: ${hashed} | Reused: ${reused}`); + } catch (e) { + toast("error", "Cache build failed", String(e?.message || e)); + } finally { + buildFingerprintBtn.textContent = originalLabel; + buildFingerprintBtn.disabled = false; + refreshFingerprintStatus(); + } + }); + + saveWorkflowBtn.addEventListener("click", async () => { + updateState(); + const wfName = workflowNameInput.value.trim(); + await saveWorkflowToProject(state, wfName); + }); + + switchBtn.addEventListener("click", async () => { + const det = state.detected; + if (!det || det.mode === "none") { + toast("warn", "No detected project", "Nothing to switch to."); + return; + } + try { + if (det.project_id) { + await setActiveProjectById(state, det.project_id); + } else if (det.project_folder) { + const resp = await fetchJSON("/mjr_project/set", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ project_name: det.project_folder, create_base: true }), + }); + state.projectId = resp.project_id; + state.projectFolder = resp.project_folder; + state.projectExists = true; + state.lastError = ""; + saveState(state); + resetForProjectChange(); + refreshProjectsList(true); + toast("success", "Project active", resp.project_folder); + } else { + toast("warn", "No detected project", "No folder available to switch."); + return; + } + state.detected = null; + updateResolve(); + state.workflowHasSignature = true; + updateWorkflowBlock(); + stampGraphProjectSignature(app, state); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Switch failed", state.lastError); + } + }); + + const assignWorkflowToActive = async () => { + if (!state.projectId || !state.projectFolder) { + toast("error", "No active project", "Set a project first."); + return; + } + stampGraphProjectSignature(app, state); + state.workflowHasSignature = true; + state.workflowPanelOpen = false; + toast("success", "Workflow assigned", state.projectFolder); + const doPatch = await psConfirm({ + title: "Patch save paths?", + message: "Patch save paths to the active project?", + }); + if (doPatch) { + const base = `PROJECTS/${state.projectFolder}`; + const imgRel = joinRel(base, mediaDir("images")); + const vidRel = joinRel(base, mediaDir("videos")); + const patchedImages = await patchSaveNodes(app, imgRel, "", "images"); + const patchedVideos = await patchSaveNodes(app, vidRel, "", "videos"); + const patched = patchedImages + patchedVideos; + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} nodes.`); + } else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + } else { + toast("info", "Patch skipped", "Save paths were not changed."); + } + state.detected = null; + updateStatus(); + updateResolve(); + updateWorkflowBlock(); + }; + + assignBtn.addEventListener("click", assignWorkflowToActive); + + createBtn.addEventListener("click", async () => { + const det = state.detected; + const suggested = det?.project_folder || "New_Project"; + const entered = await psPrompt({ + title: "Create project", + message: "Enter a project name", + defaultValue: suggested, + }); + if (entered === null) { + toast("info", "Create cancelled", "No project name provided."); + return; + } + const projectName = String(entered || "").trim(); + if (!projectName) { + toast("error", "Validation", "Project name is required."); + return; + } + try { + await createAndActivateProject(state, projectName, { + updateStatus, + updateResolve, + updateWorkflowBlock, + resetForProjectChange, + updatePreview, + refreshProjectsList, + }); + state.detected = null; + updateResolve(); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + } + }); + + dismissBtn.addEventListener("click", () => { + state.detected = null; + updateResolve(); + }); + + wfCreateBtn.addEventListener("click", async () => { + const name = pendingProjectNameInput.value.trim(); + if (!name) { + toast("error", "Validation", "Project name is required."); + return; + } + try { + await createAndActivateProject(state, name, { + updateStatus, + updateResolve, + updateWorkflowBlock, + resetForProjectChange, + updatePreview, + refreshProjectsList, + }); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + } + }); + + wfActivateBtn.addEventListener("click", async () => { + const projectId = pendingSelect.value; + if (!projectId) { + toast("error", "Validation", "Select a project first."); + return; + } + try { + await setActiveProjectById(state, projectId); + state.workflowHasSignature = true; + updateWorkflowBlock(); + stampGraphProjectSignature(app, state); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Activate failed", state.lastError); + } + }); + + wfAssignBtn.addEventListener("click", assignWorkflowToActive); + + modelSelect.addEventListener("change", updateState); + + const loadModels = async () => { + try { + const resp = await fetchJSON("/mjr_project/models"); + const categories = resp.categories || {}; + modelSelect.innerHTML = ""; + + const primaryCats = ["diffusion_models", "checkpoints"]; + const extraCats = Object.keys(categories).filter( + (c) => !primaryCats.includes(c) + ); + + const addGroup = (cat) => { + const items = categories[cat] || []; + if (!Array.isArray(items) || items.length === 0) return 0; + const group = document.createElement("optgroup"); + group.label = cat; + for (const item of items) { + const opt = document.createElement("option"); + opt.value = item; + opt.textContent = item; + group.appendChild(opt); + } + modelSelect.appendChild(group); + return items.length; + }; + + let total = 0; + for (const cat of primaryCats) { + total += addGroup(cat); + } + if (state.showMoreModels) { + for (const cat of extraCats) { + total += addGroup(cat); + } + } + + if (total === 0) { + const opt = document.createElement("option"); + opt.value = "Unknown"; + opt.textContent = "Unknown"; + modelSelect.appendChild(opt); + } + modelEmptyMsg.style.display = total === 0 ? "block" : "none"; + + if (state.modelSelection) { + modelSelect.value = state.modelSelection; + } + if (!modelSelect.value && modelSelect.options.length > 0) { + modelSelect.selectedIndex = 0; + } + updateState(); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Model list failed", state.lastError); + console.error(e); + } + }; + + modelToggleBtn.addEventListener("click", () => { + state.showMoreModels = !state.showMoreModels; + modelToggleBtn.textContent = state.showMoreModels ? "Show less" : "Show more"; + saveState(state); + loadModels(); + }); + + createApplyBtn.addEventListener("click", async () => { + if (!state.projectId) { + state.lastError = "set project first"; + updateStatus(); + toast("error", "Validation", "Set a project first."); + return; + } + const name = nameInput.value.trim(); + if (!name) { + state.lastError = "name required"; + updateStatus(); + toast("error", "Validation", "Name is required."); + return; + } + + if (useCustomCb.checked && !customModelInput.value.trim()) { + toast("error", "Validation", "Custom model is required."); + return; + } + const modelInfo = resolveModel(); + const modelName = modelInfo.modelRaw || "Unknown"; + + try { + const resp = await fetchJSON("/mjr_project/create_custom_out", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + project_id: state.projectId, + kind: kindSelect.value, + name, + media: mediaSelect.value, + model: modelName, + model_upper: modelInfo.modelUpper === true, + date: yymmddJS(), + template: templateInput.value, + }), + }); + + state.lastRelDir = resp.rel_dir; + state.lastPrefix = resp.filename_prefix; + state.media = resp.media || mediaSelect.value; + state.kind = resp.kind || kindSelect.value; + state.projectExists = true; + state.workflowHasSignature = true; + state.lastError = ""; + saveState(state); + updateStatus(); + updateWorkflowBlock(); + + targetLabel.textContent = `Target: ${resp.rel_dir} | Prefix: ${resp.filename_prefix}`; + + if (autoPatchCb.checked) { + const patched = await patchSaveNodes( + app, + resp.rel_dir, + resp.filename_prefix, + resp.media + ); + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} nodes.`); + } else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + } + stampGraphProjectSignature(app, state); + toast("success", "Folder created", resp.rel_dir); + refreshProjectsList(true); + } catch (e) { + state.lastError = String(e.message || e); + updateStatus(); + toast("error", "Create failed", state.lastError); + console.error(e); + } + }); + + patchNowBtn.addEventListener("click", async () => { + rollOutputDateIfNeeded(); + if (!state.lastRelDir || !state.lastPrefix) { + targetLabel.textContent = "Target: none (create an output)"; + toast("warn", "No target", "Create an output first."); + return; + } + + // Count save nodes first + const saveNodes = (app.graph?._nodes || []).filter(n => isSaveLikeNode(n)); + const nodeCount = saveNodes.length; + + if (nodeCount === 0) { + toast("warn", "No nodes to patch", "No save-like nodes found in workflow."); + return; + } + + // Ask for confirmation + const confirmed = await psConfirm({ + title: "Patch save paths?", + message: + `Update ${nodeCount} save node${nodeCount !== 1 ? "s" : ""} to use the project path?\n\n` + + `Path: ${state.lastRelDir}\n` + + `Prefix: ${state.lastPrefix}\n\n` + + `This can be undone with Ctrl+Z.`, + }); + + if (!confirmed) { + toast("info", "Cancelled", "No nodes were modified."); + return; + } + + const patched = await patchSaveNodes(app, state.lastRelDir, state.lastPrefix, state.media); + if (patched > 0) { + toast("success", "Patch applied", `Updated ${patched} node${patched !== 1 ? 's' : ''}.`); + } else { + toast("warn", "No nodes patched", "No save-like nodes were updated."); + } + stampGraphProjectSignature(app, state); + }); + + loadModels(); + refreshProjectsList(true); + refreshExistingNames(true); + updatePreview(); + refreshFingerprintStatus(); + if (getSettingBool(SETTING_AUTO_SCAN_MISSING_ON_OPEN, true)) { + refreshMissingStatus(true); + } else { + updateFixerStatus(); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..76fdaab --- /dev/null +++ b/tsconfig.json @@ -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"] +}