mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-25 04:54:08 -03:00
feat: support reverse-proxy URL subpaths (llama-swap, SwarmUI) (#1122)
This commit is contained in:
@@ -50,6 +50,7 @@ from ...services.errors import RateLimitError, ResourceNotFoundError
|
|||||||
from ...utils.civitai_utils import resolve_license_payload
|
from ...utils.civitai_utils import resolve_license_payload
|
||||||
from ...utils.file_utils import calculate_sha256
|
from ...utils.file_utils import calculate_sha256
|
||||||
from ...utils.metadata_manager import MetadataManager
|
from ...utils.metadata_manager import MetadataManager
|
||||||
|
from ...utils.url_utils import relative_root_prefix
|
||||||
|
|
||||||
LICENSE_FIELDS = (
|
LICENSE_FIELDS = (
|
||||||
"allowNoCredit",
|
"allowNoCredit",
|
||||||
@@ -204,6 +205,7 @@ class ModelPageView:
|
|||||||
"version": self._get_app_version(),
|
"version": self._get_app_version(),
|
||||||
"provider_presets_json": json.dumps(PROVIDER_PRESETS),
|
"provider_presets_json": json.dumps(PROVIDER_PRESETS),
|
||||||
"provider_models_json": "{}",
|
"provider_models_json": "{}",
|
||||||
|
"rel_prefix": relative_root_prefix(request.path),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not is_initializing:
|
if not is_initializing:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from ...utils.constants import NSFW_LEVELS
|
|||||||
from ...utils.directory_browser import WINDOWS_DRIVES_TOKEN, browse_directory
|
from ...utils.directory_browser import WINDOWS_DRIVES_TOKEN, browse_directory
|
||||||
from ...utils.exif_utils import ExifUtils
|
from ...utils.exif_utils import ExifUtils
|
||||||
from ...utils.recipe_open_stats import RecipeOpenStats
|
from ...utils.recipe_open_stats import RecipeOpenStats
|
||||||
|
from ...utils.url_utils import relative_root_prefix
|
||||||
from ...recipes.merger import GenParamsMerger
|
from ...recipes.merger import GenParamsMerger
|
||||||
from ...recipes.enrichment import RecipeEnricher
|
from ...recipes.enrichment import RecipeEnricher
|
||||||
from ...services.websocket_manager import ws_manager as default_ws_manager
|
from ...services.websocket_manager import ws_manager as default_ws_manager
|
||||||
@@ -215,6 +216,7 @@ class RecipePageView:
|
|||||||
settings=self._settings,
|
settings=self._settings,
|
||||||
request=request,
|
request=request,
|
||||||
t=self._server_i18n.get_translation,
|
t=self._server_i18n.get_translation,
|
||||||
|
rel_prefix=relative_root_prefix(request.path),
|
||||||
)
|
)
|
||||||
except Exception as cache_error: # pragma: no cover - logging path
|
except Exception as cache_error: # pragma: no cover - logging path
|
||||||
self._logger.error("Error loading recipe cache data: %s", cache_error)
|
self._logger.error("Error loading recipe cache data: %s", cache_error)
|
||||||
@@ -223,6 +225,7 @@ class RecipePageView:
|
|||||||
settings=self._settings,
|
settings=self._settings,
|
||||||
request=request,
|
request=request,
|
||||||
t=self._server_i18n.get_translation,
|
t=self._server_i18n.get_translation,
|
||||||
|
rel_prefix=relative_root_prefix(request.path),
|
||||||
)
|
)
|
||||||
return web.Response(text=rendered, content_type="text/html")
|
return web.Response(text=rendered, content_type="text/html")
|
||||||
except Exception as exc: # pragma: no cover - logging path
|
except Exception as exc: # pragma: no cover - logging path
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from ..services.server_i18n import server_i18n
|
|||||||
from ..services.service_registry import ServiceRegistry
|
from ..services.service_registry import ServiceRegistry
|
||||||
from ..services.model_query import normalize_sub_type, resolve_sub_type
|
from ..services.model_query import normalize_sub_type, resolve_sub_type
|
||||||
from ..utils.constants import VALID_LORA_SUB_TYPES, VALID_CHECKPOINT_SUB_TYPES
|
from ..utils.constants import VALID_LORA_SUB_TYPES, VALID_CHECKPOINT_SUB_TYPES
|
||||||
|
from ..utils.url_utils import relative_root_prefix
|
||||||
from ..utils.usage_stats import UsageStats
|
from ..utils.usage_stats import UsageStats
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -106,6 +107,7 @@ class StatsRoutes:
|
|||||||
settings=settings_manager,
|
settings=settings_manager,
|
||||||
request=request,
|
request=request,
|
||||||
t=server_i18n.get_translation,
|
t=server_i18n.get_translation,
|
||||||
|
rel_prefix=relative_root_prefix(request.path),
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""Helpers for generating URLs that survive reverse-proxy subpath mounts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def relative_root_prefix(request_path: str) -> str:
|
||||||
|
"""Return the relative prefix ("", "../", ...) that takes a manager page
|
||||||
|
back to the mount root.
|
||||||
|
|
||||||
|
Templates reference assets and pages with relative URLs (e.g.
|
||||||
|
``{{ rel_prefix }}loras_static/...``) so the browser keeps whatever
|
||||||
|
subpath a reverse proxy (llama-swap, SwarmUI, ...) mounted ComfyUI under.
|
||||||
|
The backend only ever sees the stripped path, so the depth of the page
|
||||||
|
route is all that matters: "/loras" -> "", "/loras/recipes" -> "../".
|
||||||
|
"""
|
||||||
|
|
||||||
|
segments = [segment for segment in request_path.split("/") if segment]
|
||||||
|
return "../" * max(len(segments) - 1, 0)
|
||||||
@@ -9,6 +9,7 @@ import { moveManager } from '../../managers/MoveManager.js';
|
|||||||
import { rematchModalManager } from '../../managers/RematchModalManager.js';
|
import { rematchModalManager } from '../../managers/RematchModalManager.js';
|
||||||
import { showRematchSummary } from '../RematchSummaryModal.js';
|
import { showRematchSummary } from '../RematchSummaryModal.js';
|
||||||
import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js';
|
import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js';
|
||||||
|
import { withBasePath } from '../../utils/basePath.js';
|
||||||
|
|
||||||
export class RecipeContextMenu extends BaseContextMenu {
|
export class RecipeContextMenu extends BaseContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -180,7 +181,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
|||||||
setSessionItem('filterRecipeName', recipe.title);
|
setSessionItem('filterRecipeName', recipe.title);
|
||||||
|
|
||||||
// Navigate to the LoRAs page
|
// Navigate to the LoRAs page
|
||||||
window.location.href = '/loras';
|
window.location.href = withBasePath('/loras');
|
||||||
} else {
|
} else {
|
||||||
showToast('recipes.contextMenu.viewLoras.noLorasFound', {}, 'info');
|
showToast('recipes.contextMenu.viewLoras.noLorasFound', {}, 'info');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { state } from '../state/index.js';
|
|||||||
import { setSessionItem, removeSessionItem, getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
import { setSessionItem, removeSessionItem, getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow, extractRecipeId } from '../api/recipeApi.js';
|
import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow, extractRecipeId } from '../api/recipeApi.js';
|
||||||
import { downloadManager } from '../managers/DownloadManager.js';
|
import { downloadManager } from '../managers/DownloadManager.js';
|
||||||
|
import { withBasePath } from '../utils/basePath.js';
|
||||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||||
import { openMediaViewer } from './shared/MediaViewer.js';
|
import { openMediaViewer } from './shared/MediaViewer.js';
|
||||||
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
|
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
|
||||||
@@ -3231,7 +3232,7 @@ class RecipeModal {
|
|||||||
setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title);
|
setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = '/checkpoints';
|
window.location.href = withBasePath('/checkpoints');
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCheckpointHash(checkpoint) {
|
_getCheckpointHash(checkpoint) {
|
||||||
@@ -3281,7 +3282,7 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the LoRAs page
|
// Navigate to the LoRAs page
|
||||||
window.location.href = '/loras';
|
window.location.href = withBasePath('/loras');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only in-library LoRA items are row-navigable: the row opens the local
|
// Only in-library LoRA items are row-navigable: the row opens the local
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
|
||||||
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { withBasePath } from '../../utils/basePath.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads recipes that use the specified model and renders them in the tab.
|
* Loads recipes that use the specified model and renders them in the tab.
|
||||||
@@ -356,7 +357,7 @@ function navigateToRecipesPage({ modelKind, displayName, modelHash }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Directly navigate to recipes page
|
// Directly navigate to recipes page
|
||||||
window.location.href = '/loras/recipes';
|
window.location.href = withBasePath('/loras/recipes');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,7 +379,7 @@ function navigateToRecipeDetails(recipeId) {
|
|||||||
setSessionItem('viewRecipeId', recipeId);
|
setSessionItem('viewRecipeId', recipeId);
|
||||||
|
|
||||||
// Directly navigate to recipes page
|
// Directly navigate to recipes page
|
||||||
window.location.href = '/loras/recipes';
|
window.location.href = withBasePath('/loras/recipes');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecipesEndpoint(modelKind) {
|
function getRecipesEndpoint(modelKind) {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Base-path helpers for the manager pages.
|
||||||
|
*
|
||||||
|
* `window.LM_BASE_PATH` is set by the inline bootstrap in
|
||||||
|
* templates/components/base_path_bootstrap.html: it is the reverse-proxy
|
||||||
|
* subpath the manager is served under (e.g. "/comfyui"), or "" for normal
|
||||||
|
* and standalone deployments.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getBasePath() {
|
||||||
|
return window.LM_BASE_PATH || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withBasePath(path) {
|
||||||
|
return `${getBasePath()}${path}`;
|
||||||
|
}
|
||||||
+10
-9
@@ -2,19 +2,20 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
{% include 'components/base_path_bootstrap.html' %}
|
||||||
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/loras_static/css/style.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/style.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/onboarding.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/onboarding.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/vendor/flag-icons/flag-icons.min.css">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/vendor/flag-icons/flag-icons.min.css">
|
||||||
{% block page_css %}{% endblock %}
|
{% block page_css %}{% endblock %}
|
||||||
<link rel="stylesheet" href="/loras_static/vendor/font-awesome/css/all.min.css"
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/vendor/font-awesome/css/all.min.css"
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ rel_prefix }}loras_static/images/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ rel_prefix }}loras_static/images/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
<link rel="manifest" href="{{ rel_prefix }}loras_static/images/site.webmanifest">
|
||||||
|
|
||||||
<link rel="preload" as="font" type="font/woff2" href="/loras_static/vendor/font-awesome/webfonts/fa-solid-900.woff2" crossorigin>
|
<link rel="preload" as="font" type="font/woff2" href="{{ rel_prefix }}loras_static/vendor/font-awesome/webfonts/fa-solid-900.woff2" crossorigin>
|
||||||
|
|
||||||
<!-- 添加性能监控 -->
|
<!-- 添加性能监控 -->
|
||||||
<script>
|
<script>
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
|
|
||||||
{% if is_initializing %}
|
{% if is_initializing %}
|
||||||
<!-- Load initialization JavaScript -->
|
<!-- Load initialization JavaScript -->
|
||||||
<script type="module" src="/loras_static/js/components/initialization.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/components/initialization.js?v={{ version }}"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% block main_script %}{% endblock %}
|
{% block main_script %}{% endblock %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -75,5 +75,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
<script type="module" src="/loras_static/js/checkpoints.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/checkpoints.js?v={{ version }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
{#
|
||||||
|
Base-path bootstrap. Must be the FIRST element in <head>: it detects the
|
||||||
|
reverse-proxy subpath (e.g. "/comfyui" when served via llama-swap, or
|
||||||
|
"/ComfyBackendDirect" via SwarmUI) from the current page URL and, only when
|
||||||
|
a prefix exists, patches fetch/XHR/WebSocket/innerHTML/DOM URL setters so
|
||||||
|
that root-absolute URLs ("/api/lm/...", "/loras_static/...") generated
|
||||||
|
anywhere in the frontend or in backend JSON payloads get the prefix
|
||||||
|
prepended. When no prefix is detected (normal or standalone deployment)
|
||||||
|
nothing is patched and behavior is byte-identical to before.
|
||||||
|
#}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Manager page routes as the backend sees them (proxies strip the
|
||||||
|
// prefix, so the page path always ends with one of these suffixes).
|
||||||
|
// Longest first so "/loras/recipes" wins over "/loras".
|
||||||
|
var KNOWN_PAGES = ['/loras/recipes', '/loras', '/checkpoints', '/embeddings', '/other', '/statistics'];
|
||||||
|
|
||||||
|
var path = window.location.pathname.replace(/\/+$/, '') || '/';
|
||||||
|
var prefix = '';
|
||||||
|
for (var i = 0; i < KNOWN_PAGES.length; i++) {
|
||||||
|
var page = KNOWN_PAGES[i];
|
||||||
|
if (path === page || (path.length > page.length && path.endsWith(page))) {
|
||||||
|
prefix = path.slice(0, path.length - page.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.LM_BASE_PATH = prefix;
|
||||||
|
if (!prefix) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefixBody = prefix.slice(1);
|
||||||
|
|
||||||
|
function prefixPath(p) {
|
||||||
|
if (p.charAt(0) !== '/' || p.charAt(1) === '/') {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
// Already prefixed (e.g. markup re-serialized from the DOM).
|
||||||
|
if (p === prefix || p.indexOf(prefix + '/') === 0) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return prefix + p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixUrl(url) {
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
if (url.charAt(0) === '/') {
|
||||||
|
return prefixPath(url);
|
||||||
|
}
|
||||||
|
// Absolute same-origin URL (e.g. from a Request object).
|
||||||
|
if (url.indexOf('http') === 0) {
|
||||||
|
try {
|
||||||
|
var u = new URL(url);
|
||||||
|
if (u.origin === window.location.origin) {
|
||||||
|
var prefixed = prefixPath(u.pathname);
|
||||||
|
if (prefixed !== u.pathname) {
|
||||||
|
u.pathname = prefixed;
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* not a parseable URL: leave untouched */ }
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.lmWithBasePath = prefixUrl;
|
||||||
|
|
||||||
|
// fetch()
|
||||||
|
if (window.fetch) {
|
||||||
|
var nativeFetch = window.fetch;
|
||||||
|
window.fetch = function (input, init) {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
input = prefixUrl(input);
|
||||||
|
} else if (typeof URL !== 'undefined' && input instanceof URL) {
|
||||||
|
// fetch(new URL('/api/...', location.origin)) bypasses the
|
||||||
|
// string check — coerce so same-origin URLs get prefixed.
|
||||||
|
input = prefixUrl(input.href);
|
||||||
|
} else if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||||
|
var requestUrl = prefixUrl(input.url);
|
||||||
|
if (requestUrl !== input.url) {
|
||||||
|
input = new Request(requestUrl, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nativeFetch.call(this, input, init);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// XMLHttpRequest
|
||||||
|
if (window.XMLHttpRequest) {
|
||||||
|
var nativeOpen = window.XMLHttpRequest.prototype.open;
|
||||||
|
window.XMLHttpRequest.prototype.open = function (method, url) {
|
||||||
|
arguments[1] = prefixUrl(typeof url === 'string' ? url : String(url));
|
||||||
|
return nativeOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket
|
||||||
|
if (window.WebSocket) {
|
||||||
|
var NativeWebSocket = window.WebSocket;
|
||||||
|
var PatchedWebSocket = function (url, protocols) {
|
||||||
|
if (url && typeof url !== 'string' && url.href) {
|
||||||
|
url = url.href;
|
||||||
|
}
|
||||||
|
return protocols === undefined
|
||||||
|
? new NativeWebSocket(prefixUrl(url))
|
||||||
|
: new NativeWebSocket(prefixUrl(url), protocols);
|
||||||
|
};
|
||||||
|
PatchedWebSocket.prototype = NativeWebSocket.prototype;
|
||||||
|
PatchedWebSocket.CONNECTING = NativeWebSocket.CONNECTING;
|
||||||
|
PatchedWebSocket.OPEN = NativeWebSocket.OPEN;
|
||||||
|
PatchedWebSocket.CLOSING = NativeWebSocket.CLOSING;
|
||||||
|
PatchedWebSocket.CLOSED = NativeWebSocket.CLOSED;
|
||||||
|
window.WebSocket = PatchedWebSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markup injected via innerHTML/outerHTML/insertAdjacentHTML carries
|
||||||
|
// root-absolute URLs from backend JSON (preview URLs, placeholders) and
|
||||||
|
// inline handlers (onerror="this.src='/...'"): rewrite the attributes.
|
||||||
|
var ATTR_URL_RE = /((?:src|href|poster)\s*=\s*["'])(\/)(?!\/)/g;
|
||||||
|
function rewriteMarkup(html) {
|
||||||
|
if (typeof html !== 'string' || html.indexOf('/') === -1) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
return html.replace(ATTR_URL_RE, function (match, head, slash, offset, whole) {
|
||||||
|
var rest = whole.slice(offset + match.length);
|
||||||
|
if (rest === prefixBody || rest.indexOf(prefixBody + '/') === 0) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
return head + prefix + '/';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMarkupProp(proto, prop) {
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
||||||
|
if (!desc || !desc.set) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.defineProperty(proto, prop, {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: desc.enumerable,
|
||||||
|
get: desc.get,
|
||||||
|
set: function (value) { desc.set.call(this, rewriteMarkup(value)); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.Element) {
|
||||||
|
patchMarkupProp(window.Element.prototype, 'innerHTML');
|
||||||
|
patchMarkupProp(window.Element.prototype, 'outerHTML');
|
||||||
|
var nativeInsertAdjacentHTML = window.Element.prototype.insertAdjacentHTML;
|
||||||
|
if (nativeInsertAdjacentHTML) {
|
||||||
|
window.Element.prototype.insertAdjacentHTML = function (position, html) {
|
||||||
|
return nativeInsertAdjacentHTML.call(this, position, rewriteMarkup(html));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct DOM assignments: img.src = model.preview_url, anchor.href, ...
|
||||||
|
var nativeSetAttribute = window.Element.prototype.setAttribute;
|
||||||
|
window.Element.prototype.setAttribute = function (name, value) {
|
||||||
|
if (typeof value === 'string' && /^(src|href|poster)$/i.test(name)) {
|
||||||
|
value = prefixUrl(value);
|
||||||
|
}
|
||||||
|
return nativeSetAttribute.call(this, name, value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchUrlProp(proto, prop) {
|
||||||
|
if (!proto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
||||||
|
if (!desc || !desc.set) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.defineProperty(proto, prop, {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: desc.enumerable,
|
||||||
|
get: desc.get,
|
||||||
|
set: function (value) { desc.set.call(this, prefixUrl(value)); },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
patchUrlProp(window.HTMLImageElement && window.HTMLImageElement.prototype, 'src');
|
||||||
|
patchUrlProp(window.HTMLMediaElement && window.HTMLMediaElement.prototype, 'src');
|
||||||
|
patchUrlProp(window.HTMLSourceElement && window.HTMLSourceElement.prototype, 'src');
|
||||||
|
patchUrlProp(window.HTMLVideoElement && window.HTMLVideoElement.prototype, 'poster');
|
||||||
|
patchUrlProp(window.HTMLAnchorElement && window.HTMLAnchorElement.prototype, 'href');
|
||||||
|
patchUrlProp(window.HTMLScriptElement && window.HTMLScriptElement.prototype, 'src');
|
||||||
|
patchUrlProp(window.HTMLLinkElement && window.HTMLLinkElement.prototype, 'href');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
<!-- Left section: Logo + Navigation -->
|
<!-- Left section: Logo + Navigation -->
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="header-branding">
|
<div class="header-branding">
|
||||||
<a href="/loras" class="logo-link">
|
<a href="{{ rel_prefix }}loras" class="logo-link">
|
||||||
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
<img src="{{ rel_prefix }}loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
||||||
<span class="app-title">{{ t('header.appTitle') }}</span>
|
<span class="app-title">{{ t('header.appTitle') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,26 +23,26 @@
|
|||||||
{% set current_page = 'loras' %}
|
{% set current_page = 'loras' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem">
|
<a href="{{ rel_prefix }}loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem">
|
||||||
<i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span>
|
<i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/loras/recipes" class="nav-item{% if current_path.startswith('/loras/recipes') %} active{% endif %}"
|
<a href="{{ rel_prefix }}loras/recipes" class="nav-item{% if current_path.startswith('/loras/recipes') %} active{% endif %}"
|
||||||
id="recipesNavItem">
|
id="recipesNavItem">
|
||||||
<i class="fas fa-book-open"></i> <span>{{ t('header.navigation.recipes') }}</span>
|
<i class="fas fa-book-open"></i> <span>{{ t('header.navigation.recipes') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/checkpoints" class="nav-item{% if current_path.startswith('/checkpoints') %} active{% endif %}"
|
<a href="{{ rel_prefix }}checkpoints" class="nav-item{% if current_path.startswith('/checkpoints') %} active{% endif %}"
|
||||||
id="checkpointsNavItem">
|
id="checkpointsNavItem">
|
||||||
<i class="fas fa-check-circle"></i> <span>{{ t('header.navigation.checkpoints') }}</span>
|
<i class="fas fa-check-circle"></i> <span>{{ t('header.navigation.checkpoints') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/embeddings" class="nav-item{% if current_path.startswith('/embeddings') %} active{% endif %}"
|
<a href="{{ rel_prefix }}embeddings" class="nav-item{% if current_path.startswith('/embeddings') %} active{% endif %}"
|
||||||
id="embeddingsNavItem">
|
id="embeddingsNavItem">
|
||||||
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
|
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/other" class="nav-item{% if current_path.startswith('/other') %} active{% endif %}{% if not settings.get('enable_other_models') %} nav-item--hidden{% endif %}"
|
<a href="{{ rel_prefix }}other" class="nav-item{% if current_path.startswith('/other') %} active{% endif %}{% if not settings.get('enable_other_models') %} nav-item--hidden{% endif %}"
|
||||||
id="otherNavItem">
|
id="otherNavItem">
|
||||||
<i class="fas fa-shapes"></i> <span>{{ t('header.navigation.other') }}</span>
|
<i class="fas fa-shapes"></i> <span>{{ t('header.navigation.other') }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}"
|
<a href="{{ rel_prefix }}statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}"
|
||||||
id="statisticsNavItem">
|
id="statisticsNavItem">
|
||||||
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="tip-carousel" id="tipCarousel">
|
<div class="tip-carousel" id="tipCarousel">
|
||||||
<div class="tip-item active">
|
<div class="tip-item active">
|
||||||
<div class="tip-image">
|
<div class="tip-image">
|
||||||
<img src="/loras_static/images/tips/civitai-api.png" alt="{{ t('initialization.tips.civitai.alt') }}"
|
<img src="{{ rel_prefix }}loras_static/images/tips/civitai-api.png" alt="{{ t('initialization.tips.civitai.alt') }}"
|
||||||
onerror="this.src='/loras_static/images/no-preview.png'">
|
onerror="this.src='/loras_static/images/no-preview.png'">
|
||||||
</div>
|
</div>
|
||||||
<div class="tip-text">
|
<div class="tip-text">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tip-item">
|
<div class="tip-item">
|
||||||
<div class="tip-image">
|
<div class="tip-image">
|
||||||
<img src="/loras_static/images/tips/civitai-download.png" alt="{{ t('initialization.tips.download.alt') }}"
|
<img src="{{ rel_prefix }}loras_static/images/tips/civitai-download.png" alt="{{ t('initialization.tips.download.alt') }}"
|
||||||
onerror="this.src='/loras_static/images/no-preview.png'">
|
onerror="this.src='/loras_static/images/no-preview.png'">
|
||||||
</div>
|
</div>
|
||||||
<div class="tip-text">
|
<div class="tip-text">
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tip-item">
|
<div class="tip-item">
|
||||||
<div class="tip-image">
|
<div class="tip-image">
|
||||||
<img src="/loras_static/images/tips/recipes.png" alt="{{ t('initialization.tips.recipes.alt') }}"
|
<img src="{{ rel_prefix }}loras_static/images/tips/recipes.png" alt="{{ t('initialization.tips.recipes.alt') }}"
|
||||||
onerror="this.src='/loras_static/images/no-preview.png'">
|
onerror="this.src='/loras_static/images/no-preview.png'">
|
||||||
</div>
|
</div>
|
||||||
<div class="tip-text">
|
<div class="tip-text">
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tip-item">
|
<div class="tip-item">
|
||||||
<div class="tip-image">
|
<div class="tip-image">
|
||||||
<img src="/loras_static/images/tips/filter.png" alt="{{ t('initialization.tips.filter.alt') }}"
|
<img src="{{ rel_prefix }}loras_static/images/tips/filter.png" alt="{{ t('initialization.tips.filter.alt') }}"
|
||||||
onerror="this.src='/loras_static/images/no-preview.png'">
|
onerror="this.src='/loras_static/images/no-preview.png'">
|
||||||
</div>
|
</div>
|
||||||
<div class="tip-text">
|
<div class="tip-text">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tip-item">
|
<div class="tip-item">
|
||||||
<div class="tip-image">
|
<div class="tip-image">
|
||||||
<img src="/loras_static/images/tips/search.webp" alt="{{ t('initialization.tips.search.alt') }}"
|
<img src="{{ rel_prefix }}loras_static/images/tips/search.webp" alt="{{ t('initialization.tips.search.alt') }}"
|
||||||
onerror="this.src='/loras_static/images/no-preview.png'">
|
onerror="this.src='/loras_static/images/no-preview.png'">
|
||||||
</div>
|
</div>
|
||||||
<div class="tip-text">
|
<div class="tip-text">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<h3>{{ t('help.gettingStarted.title') }}</h3>
|
<h3>{{ t('help.gettingStarted.title') }}</h3>
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<div class="video-thumbnail" data-video-id="hvKw31YpE-U">
|
<div class="video-thumbnail" data-video-id="hvKw31YpE-U">
|
||||||
<img src="/loras_static/images/video-thumbnails/getting-started.jpg" alt="Getting Started with LoRA Manager">
|
<img src="{{ rel_prefix }}loras_static/images/video-thumbnails/getting-started.jpg" alt="Getting Started with LoRA Manager">
|
||||||
<div class="video-play-overlay">
|
<div class="video-play-overlay">
|
||||||
<a href="https://www.youtube.com/watch?v=hvKw31YpE-U" target="_blank" class="external-link-btn">
|
<a href="https://www.youtube.com/watch?v=hvKw31YpE-U" target="_blank" class="external-link-btn">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<div class="video-item">
|
<div class="video-item">
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<div class="video-thumbnail" data-video-id="videoseries?list=PLU2fMdHNl8ohz1u7Ke3ooOuMbU5Y4sgoj">
|
<div class="video-thumbnail" data-video-id="videoseries?list=PLU2fMdHNl8ohz1u7Ke3ooOuMbU5Y4sgoj">
|
||||||
<img src="/loras_static/images/video-thumbnails/updates-playlist.jpg" alt="LoRA Manager Updates Playlist">
|
<img src="{{ rel_prefix }}loras_static/images/video-thumbnails/updates-playlist.jpg" alt="LoRA Manager Updates Playlist">
|
||||||
<div class="video-play-overlay">
|
<div class="video-play-overlay">
|
||||||
<a href="https://www.youtube.com/playlist?list=PLU2fMdHNl8ohz1u7Ke3ooOuMbU5Y4sgoj" target="_blank" class="external-link-btn">
|
<a href="https://www.youtube.com/playlist?list=PLU2fMdHNl8ohz1u7Ke3ooOuMbU5Y4sgoj" target="_blank" class="external-link-btn">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<i class="fas fa-chevron-down toggle-icon"></i>
|
<i class="fas fa-chevron-down toggle-icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="qrcode-container" id="qrCodeContainer">
|
<div class="qrcode-container" id="qrCodeContainer">
|
||||||
<img src="/loras_static/images/wechat-qr.webp" alt="WeChat Pay QR Code" class="qrcode-image">
|
<img src="{{ rel_prefix }}loras_static/images/wechat-qr.webp" alt="WeChat Pay QR Code" class="qrcode-image">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -71,5 +71,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
<script type="module" src="/loras_static/js/embeddings.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/embeddings.js?v={{ version }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if not is_initializing %}
|
{% if not is_initializing %}
|
||||||
<script type="module" src="/loras_static/js/loras.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/loras.js?v={{ version }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -167,8 +167,8 @@
|
|||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if other_disabled or other_no_paths %}
|
{% if other_disabled or other_no_paths %}
|
||||||
<script type="module" src="/loras_static/js/other_disabled.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/other_disabled.js?v={{ version }}"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/other.js?v={{ version }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
{% block page_id %}recipes{% endblock %}
|
{% block page_id %}recipes{% endblock %}
|
||||||
|
|
||||||
{% block page_css %}
|
{% block page_css %}
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/card.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/card.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/recipe-modal.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/import-modal.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/batch-import-modal.css?v={{ version }}">
|
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/batch-import-modal.css?v={{ version }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_components %}
|
{% block additional_components %}
|
||||||
@@ -113,5 +113,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
<script type="module" src="/loras_static/js/recipes.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/recipes.js?v={{ version }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block head_scripts %}
|
{% block head_scripts %}
|
||||||
<!-- Add Chart.js for statistics page -->
|
<!-- Add Chart.js for statistics page -->
|
||||||
<script src="/loras_static/vendor/chart.js/chart.umd.js"></script>
|
<script src="{{ rel_prefix }}loras_static/vendor/chart.js/chart.umd.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block init_title %}{{ t('initialization.statistics.title') }}{% endblock %}
|
{% block init_title %}{{ t('initialization.statistics.title') }}{% endblock %}
|
||||||
@@ -192,6 +192,6 @@
|
|||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if not is_initializing %}
|
{% if not is_initializing %}
|
||||||
<script type="module" src="/loras_static/js/statistics.js?v={{ version }}"></script>
|
<script type="module" src="{{ rel_prefix }}loras_static/js/statistics.js?v={{ version }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
import { getBasePath, withBasePath } from '../../../static/js/utils/basePath.js';
|
||||||
|
|
||||||
|
describe('static/js/utils/basePath.js', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.LM_BASE_PATH;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty base path when bootstrap did not set one', () => {
|
||||||
|
expect(getBasePath()).toBe('');
|
||||||
|
expect(withBasePath('/loras')).toBe('/loras');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prepends the detected base path', () => {
|
||||||
|
window.LM_BASE_PATH = '/comfyui';
|
||||||
|
expect(getBasePath()).toBe('/comfyui');
|
||||||
|
expect(withBasePath('/loras')).toBe('/comfyui/loras');
|
||||||
|
expect(withBasePath('/loras/recipes')).toBe('/comfyui/loras/recipes');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
|
||||||
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
||||||
|
|
||||||
|
const extractBootstrapScript = () => {
|
||||||
|
const html = readFileSync(
|
||||||
|
resolve(repoRoot, 'templates/components/base_path_bootstrap.html'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const match = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('bootstrap <script> block not found');
|
||||||
|
}
|
||||||
|
return match[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATCHED_PROTOS = () => [
|
||||||
|
[Element.prototype, ['innerHTML', 'outerHTML']],
|
||||||
|
[HTMLImageElement.prototype, ['src']],
|
||||||
|
[HTMLMediaElement.prototype, ['src']],
|
||||||
|
[HTMLSourceElement.prototype, ['src']],
|
||||||
|
[HTMLVideoElement.prototype, ['poster']],
|
||||||
|
[HTMLAnchorElement.prototype, ['href']],
|
||||||
|
[HTMLScriptElement.prototype, ['src']],
|
||||||
|
[HTMLLinkElement.prototype, ['href']],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('base_path_bootstrap.html', () => {
|
||||||
|
let savedGlobals;
|
||||||
|
let savedDescriptors;
|
||||||
|
let savedMethods;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
savedGlobals = {
|
||||||
|
fetch: window.fetch,
|
||||||
|
WebSocket: window.WebSocket,
|
||||||
|
};
|
||||||
|
savedDescriptors = PATCHED_PROTOS().flatMap(([proto, props]) =>
|
||||||
|
props.map((prop) => [proto, prop, Object.getOwnPropertyDescriptor(proto, prop)]),
|
||||||
|
);
|
||||||
|
savedMethods = {
|
||||||
|
xhrOpen: XMLHttpRequest.prototype.open,
|
||||||
|
insertAdjacentHTML: Element.prototype.insertAdjacentHTML,
|
||||||
|
setAttribute: Element.prototype.setAttribute,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.fetch = savedGlobals.fetch;
|
||||||
|
window.WebSocket = savedGlobals.WebSocket;
|
||||||
|
for (const [proto, prop, desc] of savedDescriptors) {
|
||||||
|
if (desc) {
|
||||||
|
Object.defineProperty(proto, prop, desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
XMLHttpRequest.prototype.open = savedMethods.xhrOpen;
|
||||||
|
Element.prototype.insertAdjacentHTML = savedMethods.insertAdjacentHTML;
|
||||||
|
Element.prototype.setAttribute = savedMethods.setAttribute;
|
||||||
|
delete window.LM_BASE_PATH;
|
||||||
|
delete window.lmWithBasePath;
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
const runBootstrap = (pathname) => {
|
||||||
|
window.history.replaceState({}, '', pathname);
|
||||||
|
(0, eval)(extractBootstrapScript());
|
||||||
|
};
|
||||||
|
|
||||||
|
it('detects no prefix for root-mounted pages and patches nothing', () => {
|
||||||
|
const nativeFetch = window.fetch;
|
||||||
|
runBootstrap('/loras');
|
||||||
|
expect(window.LM_BASE_PATH).toBe('');
|
||||||
|
expect(window.fetch).toBe(nativeFetch);
|
||||||
|
|
||||||
|
runBootstrap('/');
|
||||||
|
expect(window.LM_BASE_PATH).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['/comfyui/loras', '/comfyui'],
|
||||||
|
['/comfyui/loras/', '/comfyui'],
|
||||||
|
['/comfyui/loras/recipes', '/comfyui'],
|
||||||
|
['/comfyui/checkpoints', '/comfyui'],
|
||||||
|
['/comfyui/embeddings', '/comfyui'],
|
||||||
|
['/comfyui/other', '/comfyui'],
|
||||||
|
['/comfyui/statistics', '/comfyui'],
|
||||||
|
['/ComfyBackendDirect/loras', '/ComfyBackendDirect'],
|
||||||
|
['/proxy/nested/loras', '/proxy/nested'],
|
||||||
|
])('detects prefix for %s', (pathname, expected) => {
|
||||||
|
runBootstrap(pathname);
|
||||||
|
expect(window.LM_BASE_PATH).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mistake similar paths for manager pages', () => {
|
||||||
|
runBootstrap('/comfyui/lorasgallery');
|
||||||
|
expect(window.LM_BASE_PATH).toBe('');
|
||||||
|
runBootstrap('/comfyui/foo-loras');
|
||||||
|
expect(window.LM_BASE_PATH).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefixes root-absolute fetch URLs only', async () => {
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||||
|
window.fetch = fetchSpy;
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
await window.fetch('/api/lm/loras/list');
|
||||||
|
await window.fetch('/loras_static/images/no-preview.png');
|
||||||
|
await window.fetch('https://civitai.com/api/v1/models');
|
||||||
|
await window.fetch('//cdn.example.com/x.js');
|
||||||
|
await window.fetch('relative/path');
|
||||||
|
|
||||||
|
expect(fetchSpy.mock.calls.map((call) => call[0])).toEqual([
|
||||||
|
'/comfyui/api/lm/loras/list',
|
||||||
|
'/comfyui/loras_static/images/no-preview.png',
|
||||||
|
'https://civitai.com/api/v1/models',
|
||||||
|
'//cdn.example.com/x.js',
|
||||||
|
'relative/path',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefixes same-origin absolute fetch URLs', async () => {
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||||
|
window.fetch = fetchSpy;
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
const absolute = `${window.location.origin}/api/lm/init-status`;
|
||||||
|
await window.fetch(absolute);
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(
|
||||||
|
`${window.location.origin}/comfyui/api/lm/init-status`,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefixes fetch() called with a URL object', async () => {
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||||
|
window.fetch = fetchSpy;
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
await window.fetch(new URL('/api/lm/base-models', window.location.origin));
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(
|
||||||
|
`${window.location.origin}/comfyui/api/lm/base-models`,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
await window.fetch(new URL('https://civitai.com/api/v1/models'));
|
||||||
|
expect(fetchSpy).toHaveBeenLastCalledWith('https://civitai.com/api/v1/models', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefixes WebSocket URLs', () => {
|
||||||
|
const constructed = [];
|
||||||
|
class FakeWebSocket {
|
||||||
|
constructor(url, protocols) {
|
||||||
|
constructed.push([url, protocols]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FakeWebSocket.CONNECTING = 0;
|
||||||
|
FakeWebSocket.OPEN = 1;
|
||||||
|
FakeWebSocket.CLOSING = 2;
|
||||||
|
FakeWebSocket.CLOSED = 3;
|
||||||
|
window.WebSocket = FakeWebSocket;
|
||||||
|
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
new window.WebSocket('/ws/fetch-progress');
|
||||||
|
new window.WebSocket('wss://other.example.com/socket', ['a']);
|
||||||
|
expect(constructed).toEqual([
|
||||||
|
['/comfyui/ws/fetch-progress', undefined],
|
||||||
|
['wss://other.example.com/socket', ['a']],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rewrites root-absolute URLs inside innerHTML markup', () => {
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = `<img src="/api/lm/previews?path=x" onerror="this.src='/loras_static/images/no-preview.png'">`
|
||||||
|
+ `<a href="/api/lm/download-model/1">dl</a>`
|
||||||
|
+ `<video poster="/loras_static/p.png"><source src="/example_images_static/a/b.mp4"></video>`;
|
||||||
|
|
||||||
|
const html = container.innerHTML;
|
||||||
|
expect(html).toContain('src="/comfyui/api/lm/previews?path=x"');
|
||||||
|
expect(html).toContain("this.src='/comfyui/loras_static/images/no-preview.png'");
|
||||||
|
expect(html).toContain('href="/comfyui/api/lm/download-model/1"');
|
||||||
|
expect(html).toContain('poster="/comfyui/loras_static/p.png"');
|
||||||
|
expect(html).toContain('src="/comfyui/example_images_static/a/b.mp4"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not double-prefix markup that already carries the prefix', () => {
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = '<img src="/api/lm/previews?path=x">';
|
||||||
|
const once = container.innerHTML;
|
||||||
|
container.innerHTML = once;
|
||||||
|
expect(container.innerHTML).toBe(once);
|
||||||
|
expect(once).not.toContain('/comfyui/comfyui/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefixes direct DOM URL assignments', () => {
|
||||||
|
runBootstrap('/comfyui/loras');
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = '/loras_static/images/no-preview.png';
|
||||||
|
expect(img.getAttribute('src')).toBe('/comfyui/loras_static/images/no-preview.png');
|
||||||
|
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.setAttribute('href', '/api/lm/download-model/1');
|
||||||
|
expect(anchor.getAttribute('href')).toBe('/comfyui/api/lm/download-model/1');
|
||||||
|
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.poster = '/loras_static/p.png';
|
||||||
|
expect(video.getAttribute('poster')).toBe('/comfyui/loras_static/p.png');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
import { getComfyUIBasePath, lmUrl } from '../../../web/comfyui/base_path.js';
|
||||||
|
|
||||||
|
describe('web/comfyui/base_path.js', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['/', ''],
|
||||||
|
['/comfyui/', '/comfyui'],
|
||||||
|
['/comfyui', '/comfyui'],
|
||||||
|
['/ComfyBackendDirect/', '/ComfyBackendDirect'],
|
||||||
|
])('maps %s to base path %s', (pathname, expected) => {
|
||||||
|
window.history.replaceState({}, '', pathname);
|
||||||
|
expect(getComfyUIBasePath()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds prefixed URLs', () => {
|
||||||
|
window.history.replaceState({}, '', '/comfyui/');
|
||||||
|
expect(lmUrl('/api/lm/version-info')).toBe('/comfyui/api/lm/version-info');
|
||||||
|
expect(lmUrl('/loras')).toBe('/comfyui/loras');
|
||||||
|
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
expect(lmUrl('/api/lm/version-info')).toBe('/api/lm/version-info');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -55,10 +55,10 @@ async def test_model_page_view_reads_version_per_request():
|
|||||||
)
|
)
|
||||||
|
|
||||||
view._get_app_version = lambda: "1.0.2-old"
|
view._get_app_version = lambda: "1.0.2-old"
|
||||||
first = await view.handle(SimpleNamespace()) # pyright: ignore[reportArgumentType]
|
first = await view.handle(SimpleNamespace(path="/loras")) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
view._get_app_version = lambda: "1.0.2-new"
|
view._get_app_version = lambda: "1.0.2-new"
|
||||||
second = await view.handle(SimpleNamespace()) # pyright: ignore[reportArgumentType]
|
second = await view.handle(SimpleNamespace(path="/loras")) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
assert first.text == "1.0.2-old"
|
assert first.text == "1.0.2-old"
|
||||||
assert second.text == "1.0.2-new"
|
assert second.text == "1.0.2-new"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Tests for py/utils/url_utils.py relative_root_prefix."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from py.utils.url_utils import relative_root_prefix
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("request_path", "expected"),
|
||||||
|
[
|
||||||
|
("/", ""),
|
||||||
|
("/loras", ""),
|
||||||
|
("/checkpoints", ""),
|
||||||
|
("/statistics", ""),
|
||||||
|
("/loras/", ""),
|
||||||
|
("/loras/recipes", "../"),
|
||||||
|
("/loras/recipes/", "../"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_relative_root_prefix(request_path: str, expected: str) -> None:
|
||||||
|
assert relative_root_prefix(request_path) == expected
|
||||||
@@ -58,6 +58,7 @@
|
|||||||
import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
|
import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
|
||||||
import ModalWrapper from '../lora-pool/modals/ModalWrapper.vue'
|
import ModalWrapper from '../lora-pool/modals/ModalWrapper.vue'
|
||||||
import type { LoraItem } from '../../composables/types'
|
import type { LoraItem } from '../../composables/types'
|
||||||
|
import { lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
interface LoraListItem {
|
interface LoraListItem {
|
||||||
index: number
|
index: number
|
||||||
@@ -131,7 +132,7 @@ const selectLora = (index: number) => {
|
|||||||
// in the Vue widgets build, so we need to use the full path with /api prefix
|
// in the Vue widgets build, so we need to use the full path with /api prefix
|
||||||
const customPreviewUrlResolver = async (modelName: string) => {
|
const customPreviewUrlResolver = async (modelName: string) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/lm/loras/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`
|
lmApiUrl(`/api/lm/loras/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`)
|
||||||
)
|
)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch preview URL')
|
throw new Error('Failed to fetch preview URL')
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { LoraEntry } from '../../composables/types'
|
import type { LoraEntry } from '../../composables/types'
|
||||||
|
import { lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loras: LoraEntry[]
|
loras: LoraEntry[]
|
||||||
@@ -48,7 +49,7 @@ const previewUrls = ref<Record<string, string>>({})
|
|||||||
// Fetch preview URL for a lora using API
|
// Fetch preview URL for a lora using API
|
||||||
const fetchPreviewUrl = async (loraName: string) => {
|
const fetchPreviewUrl = async (loraName: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/preview-url?name=${encodeURIComponent(loraName)}`)
|
const response = await fetch(lmApiUrl(`/api/lm/loras/preview-url?name=${encodeURIComponent(loraName)}`))
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from './types'
|
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from './types'
|
||||||
|
import { lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
export interface CyclerLoraItem {
|
export interface CyclerLoraItem {
|
||||||
file_name: string
|
file_name: string
|
||||||
@@ -173,7 +174,7 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
|
|||||||
requestBody.pool_config = poolConfig.filters
|
requestBody.pool_config = poolConfig.filters
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/lm/loras/cycler-list', {
|
const response = await fetch(lmApiUrl('/api/lm/loras/cycler-list'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { BaseModelOption, TagOption, FolderTreeNode, LoraItem } from './types'
|
import type { BaseModelOption, TagOption, FolderTreeNode, LoraItem } from './types'
|
||||||
|
import { lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
export function useLoraPoolApi() {
|
export function useLoraPoolApi() {
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
const fetchBaseModels = async (limit = 50): Promise<BaseModelOption[]> => {
|
const fetchBaseModels = async (limit = 50): Promise<BaseModelOption[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/base-models?limit=${limit}`)
|
const response = await fetch(lmApiUrl(`/api/lm/loras/base-models?limit=${limit}`))
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return data.base_models || []
|
return data.base_models || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -17,7 +18,7 @@ export function useLoraPoolApi() {
|
|||||||
|
|
||||||
const fetchTags = async (limit = 0): Promise<TagOption[]> => {
|
const fetchTags = async (limit = 0): Promise<TagOption[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/top-tags?limit=${limit}`)
|
const response = await fetch(lmApiUrl(`/api/lm/loras/top-tags?limit=${limit}`))
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return data.tags || []
|
return data.tags || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -28,7 +29,7 @@ export function useLoraPoolApi() {
|
|||||||
|
|
||||||
const fetchFolderTree = async (): Promise<FolderTreeNode[]> => {
|
const fetchFolderTree = async (): Promise<FolderTreeNode[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/lm/loras/unified-folder-tree')
|
const response = await fetch(lmApiUrl('/api/lm/loras/unified-folder-tree'))
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
return transformFolderTree(data.tree || {})
|
return transformFolderTree(data.tree || {})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -102,7 +103,7 @@ export function useLoraPoolApi() {
|
|||||||
urlParams.set('name_pattern_use_regex', String(params.namePatternsUseRegex))
|
urlParams.set('name_pattern_use_regex', String(params.namePatternsUseRegex))
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/lm/loras/list?${urlParams}`)
|
const response = await fetch(lmApiUrl(`/api/lm/loras/list?${urlParams}`))
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { ComponentWidget, RandomizerConfig, LoraEntry } from './types'
|
import type { ComponentWidget, RandomizerConfig, LoraEntry } from './types'
|
||||||
|
import { lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>) {
|
export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>) {
|
||||||
// Flag to prevent infinite loops during config restoration
|
// Flag to prevent infinite loops during config restoration
|
||||||
@@ -160,7 +161,7 @@ export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call API endpoint
|
// Call API endpoint
|
||||||
const response = await fetch('/api/lm/loras/random-sample', {
|
const response = await fetch(lmApiUrl('/api/lm/loras/random-sample'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function getLmBasePath(): string {
|
||||||
|
const { pathname } = window.location;
|
||||||
|
return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lmApiUrl(path: string): string {
|
||||||
|
return `${getLmBasePath()}${path}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
import { getLmBasePath, lmApiUrl } from '@/utils/basePath'
|
||||||
|
|
||||||
|
const originalPathname = window.location.pathname
|
||||||
|
|
||||||
|
function setPathname(pathname: string) {
|
||||||
|
window.history.replaceState(null, '', pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setPathname(originalPathname)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getLmBasePath', () => {
|
||||||
|
it('returns an empty string when ComfyUI is served at the root', () => {
|
||||||
|
setPathname('/')
|
||||||
|
|
||||||
|
expect(getLmBasePath()).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('strips the trailing slash from a subpath prefix', () => {
|
||||||
|
setPathname('/comfyui/')
|
||||||
|
|
||||||
|
expect(getLmBasePath()).toBe('/comfyui')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps a prefix without a trailing slash as-is', () => {
|
||||||
|
setPathname('/ComfyBackendDirect')
|
||||||
|
|
||||||
|
expect(getLmBasePath()).toBe('/ComfyBackendDirect')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('lmApiUrl', () => {
|
||||||
|
it('leaves root-absolute paths unchanged at the root', () => {
|
||||||
|
setPathname('/')
|
||||||
|
|
||||||
|
expect(lmApiUrl('/api/lm/loras/list')).toBe('/api/lm/loras/list')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prepends the subpath prefix to root-absolute paths', () => {
|
||||||
|
setPathname('/comfyui/')
|
||||||
|
|
||||||
|
expect(lmApiUrl('/api/lm/loras/list')).toBe('/comfyui/api/lm/loras/list')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Base-path helpers for code running inside the ComfyUI page.
|
||||||
|
*
|
||||||
|
* When ComfyUI is served under a URL subpath by a reverse proxy (e.g.
|
||||||
|
* llama-swap serves it at "/comfyui/", SwarmUI at "/ComfyBackendDirect/"),
|
||||||
|
* the ComfyUI SPA always sits at the root of that prefix, so the prefix is
|
||||||
|
* the current pathname without the trailing slash — the same convention the
|
||||||
|
* ComfyUI frontend uses for its own api_base. Proxies strip the prefix and
|
||||||
|
* forward no headers, so the page URL is the only place to learn it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getComfyUIBasePath() {
|
||||||
|
const { pathname } = window.location;
|
||||||
|
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lmUrl(path) {
|
||||||
|
return `${getComfyUIBasePath()}${path}`;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
|
|
||||||
// Mirrors the backend resolver (get_lora_info_absolute): a ".ckpt"/".pt"
|
// Mirrors the backend resolver (get_lora_info_absolute): a ".ckpt"/".pt"
|
||||||
// reference resolves to the same-named .safetensors file. The scanner only
|
// reference resolves to the same-named .safetensors file. The scanner only
|
||||||
@@ -394,7 +395,7 @@ function connectLibraryChangeSocket() {
|
|||||||
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
let ws;
|
let ws;
|
||||||
try {
|
try {
|
||||||
ws = new WebSocket(`${protocol}${window.location.host}/ws/fetch-progress`);
|
ws = new WebSocket(`${protocol}${window.location.host}${lmUrl("/ws/fetch-progress")}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -474,7 +475,7 @@ export async function saveRecipeDirectly() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send the request to the backend API
|
// Send the request to the backend API
|
||||||
const response = await fetch('/api/lm/recipes/save-from-widget', {
|
const response = await fetch(lmUrl('/api/lm/recipes/save-from-widget'), {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
|
|
||||||
const LICENSE_ICON_PATH = "/loras_static/images/tabler/";
|
const LICENSE_ICON_PATH = lmUrl("/loras_static/images/tabler/");
|
||||||
const LICENSE_FLAG_BITS = {
|
const LICENSE_FLAG_BITS = {
|
||||||
allowNoCredit: 1 << 0,
|
allowNoCredit: 1 << 0,
|
||||||
allowOnImages: 1 << 1,
|
allowOnImages: 1 << 1,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Setting IDs and Defaults
|
// Setting IDs and Defaults
|
||||||
@@ -55,7 +56,7 @@ const loadWorkflowOptions = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/lm/example-workflows");
|
const response = await fetch(lmUrl("/api/lm/example-workflows"));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success && data.workflows) {
|
if (data.success && data.workflows) {
|
||||||
workflowOptionsFull = data.workflows;
|
workflowOptionsFull = data.workflows;
|
||||||
@@ -81,7 +82,7 @@ const loadTemplateWorkflow = async (templateName) => {
|
|||||||
const workflow = workflowOptionsFull.find((w) => w.label === templateName);
|
const workflow = workflowOptionsFull.find((w) => w.label === templateName);
|
||||||
if (workflow && workflow.value) {
|
if (workflow && workflow.value) {
|
||||||
const workflowResponse = await fetch(
|
const workflowResponse = await fetch(
|
||||||
`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`
|
lmUrl(`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`)
|
||||||
);
|
);
|
||||||
const workflowData = await workflowResponse.json();
|
const workflowData = await workflowResponse.json();
|
||||||
if (workflowData.success && workflowData.workflow) {
|
if (workflowData.success && workflowData.workflow) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
|
|
||||||
const BUTTON_TOOLTIP = "Launch LoRA Manager (Shift+Click opens in new window)";
|
const BUTTON_TOOLTIP = "Launch LoRA Manager (Shift+Click opens in new window)";
|
||||||
const LORA_MANAGER_PATH = "/loras";
|
const LORA_MANAGER_PATH = "/loras";
|
||||||
@@ -9,7 +10,7 @@ const BUTTON_GROUP_CLASS = "lora-manager-top-menu-group";
|
|||||||
const MIN_VERSION_FOR_ACTION_BAR = [1, 33, 9];
|
const MIN_VERSION_FOR_ACTION_BAR = [1, 33, 9];
|
||||||
|
|
||||||
const openLoraManager = (event) => {
|
const openLoraManager = (event) => {
|
||||||
const url = `${window.location.origin}${LORA_MANAGER_PATH}`;
|
const url = `${window.location.origin}${lmUrl(LORA_MANAGER_PATH)}`;
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
window.open(url, "_blank", NEW_WINDOW_FEATURES);
|
window.open(url, "_blank", NEW_WINDOW_FEATURES);
|
||||||
@@ -29,7 +30,7 @@ const getComfyUIFrontendVersion = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/system_stats");
|
const response = await fetch(lmUrl("/system_stats"));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data?.system?.comfyui_frontend_version) {
|
if (data?.system?.comfyui_frontend_version) {
|
||||||
@@ -80,7 +81,7 @@ const supportsActionBarButtons = async () => {
|
|||||||
|
|
||||||
const fetchVersionInfo = async () => {
|
const fetchVersionInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/lm/version-info");
|
const response = await fetch(lmUrl("/api/lm/version-info"));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ComfyUI extension to track model usage statistics
|
// ComfyUI extension to track model usage statistics
|
||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
import { showToast } from "./utils.js";
|
import { showToast } from "./utils.js";
|
||||||
import { getAutoPathCorrectionPreference, getUsageStatisticsPreference } from "./settings.js";
|
import { getAutoPathCorrectionPreference, getUsageStatisticsPreference } from "./settings.js";
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ app.registerExtension({
|
|||||||
async updateUsageStats(promptId) {
|
async updateUsageStats(promptId) {
|
||||||
try {
|
try {
|
||||||
// Call backend endpoint with the prompt_id
|
// Call backend endpoint with the prompt_id
|
||||||
const response = await fetch(`/api/lm/update-usage-stats`, {
|
const response = await fetch(lmUrl(`/api/lm/update-usage-stats`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const CONVERTED_TYPE = 'converted-widget';
|
export const CONVERTED_TYPE = 'converted-widget';
|
||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { AutoComplete } from "./autocomplete.js";
|
import { AutoComplete } from "./autocomplete.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
|
|
||||||
const ROOT_GRAPH_ID = "root";
|
const ROOT_GRAPH_ID = "root";
|
||||||
|
|
||||||
@@ -408,7 +409,7 @@ export function updateConnectedTriggerWords(node, loraNames) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch("/api/lm/loras/get_trigger_words", {
|
fetch(lmUrl("/api/lm/loras/get_trigger_words"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1105,7 +1105,7 @@ to { transform: rotate(360deg);
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-used-preview[data-v-b940502e] {
|
.last-used-preview[data-v-7db61bc9] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -1113,7 +1113,7 @@ to { transform: rotate(360deg);
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 280px;
|
width: 280px;
|
||||||
}
|
}
|
||||||
.last-used-preview__content[data-v-b940502e] {
|
.last-used-preview__content[data-v-7db61bc9] {
|
||||||
background: var(--comfy-menu-bg, #1a1a1a);
|
background: var(--comfy-menu-bg, #1a1a1a);
|
||||||
border: 1px solid var(--border-color, #444);
|
border: 1px solid var(--border-color, #444);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -1123,7 +1123,7 @@ to { transform: rotate(360deg);
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.last-used-preview__item[data-v-b940502e] {
|
.last-used-preview__item[data-v-7db61bc9] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1131,7 +1131,7 @@ to { transform: rotate(360deg);
|
|||||||
background: var(--comfy-input-bg, #333);
|
background: var(--comfy-input-bg, #333);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.last-used-preview__thumb[data-v-b940502e] {
|
.last-used-preview__thumb[data-v-7db61bc9] {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
@@ -1139,37 +1139,37 @@ to { transform: rotate(360deg);
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
.last-used-preview__thumb--placeholder[data-v-b940502e] {
|
.last-used-preview__thumb--placeholder[data-v-7db61bc9] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
.last-used-preview__thumb--placeholder svg[data-v-b940502e] {
|
.last-used-preview__thumb--placeholder svg[data-v-7db61bc9] {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
.last-used-preview__info[data-v-b940502e] {
|
.last-used-preview__info[data-v-7db61bc9] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.last-used-preview__name[data-v-b940502e] {
|
.last-used-preview__name[data-v-7db61bc9] {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.last-used-preview__strength[data-v-b940502e] {
|
.last-used-preview__strength[data-v-7db61bc9] {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.last-used-preview__more[data-v-b940502e] {
|
.last-used-preview__more[data-v-7db61bc9] {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -1957,10 +1957,10 @@ to { transform: rotate(360deg);
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container[data-v-83f6f852] {
|
.search-container[data-v-ed10780a] {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.search-icon[data-v-83f6f852] {
|
.search-icon[data-v-ed10780a] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -1970,7 +1970,7 @@ to { transform: rotate(360deg);
|
|||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.search-input[data-v-83f6f852] {
|
.search-input[data-v-ed10780a] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 32px;
|
padding: 8px 32px;
|
||||||
background: var(--comfy-input-bg, #333);
|
background: var(--comfy-input-bg, #333);
|
||||||
@@ -1981,14 +1981,14 @@ to { transform: rotate(360deg);
|
|||||||
outline: none;
|
outline: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.search-input[data-v-83f6f852]:focus {
|
.search-input[data-v-ed10780a]:focus {
|
||||||
border-color: rgba(66, 153, 225, 0.6);
|
border-color: rgba(66, 153, 225, 0.6);
|
||||||
}
|
}
|
||||||
.search-input[data-v-83f6f852]::placeholder {
|
.search-input[data-v-ed10780a]::placeholder {
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
.clear-button[data-v-83f6f852] {
|
.clear-button[data-v-ed10780a] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -2005,22 +2005,22 @@ to { transform: rotate(360deg);
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
.clear-button[data-v-83f6f852]:hover {
|
.clear-button[data-v-ed10780a]:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.clear-button svg[data-v-83f6f852] {
|
.clear-button svg[data-v-ed10780a] {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
}
|
}
|
||||||
.lora-list[data-v-83f6f852] {
|
.lora-list[data-v-ed10780a] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.lora-item[data-v-83f6f852] {
|
.lora-item[data-v-ed10780a] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -2030,14 +2030,14 @@ to { transform: rotate(360deg);
|
|||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
}
|
}
|
||||||
.lora-item[data-v-83f6f852]:hover {
|
.lora-item[data-v-ed10780a]:hover {
|
||||||
background: rgba(66, 153, 225, 0.15);
|
background: rgba(66, 153, 225, 0.15);
|
||||||
}
|
}
|
||||||
.lora-item.active[data-v-83f6f852] {
|
.lora-item.active[data-v-ed10780a] {
|
||||||
background: rgba(66, 153, 225, 0.25);
|
background: rgba(66, 153, 225, 0.25);
|
||||||
border-left-color: rgba(66, 153, 225, 0.8);
|
border-left-color: rgba(66, 153, 225, 0.8);
|
||||||
}
|
}
|
||||||
.lora-index[data-v-83f6f852] {
|
.lora-index[data-v-ed10780a] {
|
||||||
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(226, 232, 240, 0.5);
|
color: rgba(226, 232, 240, 0.5);
|
||||||
@@ -2045,7 +2045,7 @@ to { transform: rotate(360deg);
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.lora-name[data-v-83f6f852] {
|
.lora-name[data-v-ed10780a] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
@@ -2053,7 +2053,7 @@ to { transform: rotate(360deg);
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.current-badge[data-v-83f6f852] {
|
.current-badge[data-v-ed10780a] {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
background: rgba(66, 153, 225, 0.3);
|
background: rgba(66, 153, 225, 0.3);
|
||||||
@@ -2062,14 +2062,14 @@ to { transform: rotate(360deg);
|
|||||||
color: rgba(191, 219, 254, 1);
|
color: rgba(191, 219, 254, 1);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.lora-item.no-lora-item .lora-name[data-v-83f6f852] {
|
.lora-item.no-lora-item .lora-name[data-v-ed10780a] {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: rgba(226, 232, 240, 0.6);
|
color: rgba(226, 232, 240, 0.6);
|
||||||
}
|
}
|
||||||
.lora-item.no-lora-item:hover .lora-name[data-v-83f6f852] {
|
.lora-item.no-lora-item:hover .lora-name[data-v-ed10780a] {
|
||||||
color: rgba(226, 232, 240, 0.8);
|
color: rgba(226, 232, 240, 0.8);
|
||||||
}
|
}
|
||||||
.no-results[data-v-83f6f852] {
|
.no-results[data-v-ed10780a] {
|
||||||
padding: 32px 20px;
|
padding: 32px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--fg-color, #fff);
|
color: var(--fg-color, #fff);
|
||||||
@@ -12262,11 +12262,18 @@ const _sfc_main$c = /* @__PURE__ */ defineComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const FoldersModal = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["__scopeId", "data-v-046dcbf4"]]);
|
const FoldersModal = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["__scopeId", "data-v-046dcbf4"]]);
|
||||||
|
function getLmBasePath() {
|
||||||
|
const { pathname } = window.location;
|
||||||
|
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||||
|
}
|
||||||
|
function lmApiUrl(path) {
|
||||||
|
return `${getLmBasePath()}${path}`;
|
||||||
|
}
|
||||||
function useLoraPoolApi() {
|
function useLoraPoolApi() {
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const fetchBaseModels = async (limit = 50) => {
|
const fetchBaseModels = async (limit = 50) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/base-models?limit=${limit}`);
|
const response = await fetch(lmApiUrl(`/api/lm/loras/base-models?limit=${limit}`));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.base_models || [];
|
return data.base_models || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -12276,7 +12283,7 @@ function useLoraPoolApi() {
|
|||||||
};
|
};
|
||||||
const fetchTags = async (limit = 0) => {
|
const fetchTags = async (limit = 0) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/top-tags?limit=${limit}`);
|
const response = await fetch(lmApiUrl(`/api/lm/loras/top-tags?limit=${limit}`));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.tags || [];
|
return data.tags || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -12286,7 +12293,7 @@ function useLoraPoolApi() {
|
|||||||
};
|
};
|
||||||
const fetchFolderTree = async () => {
|
const fetchFolderTree = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/lm/loras/unified-folder-tree");
|
const response = await fetch(lmApiUrl("/api/lm/loras/unified-folder-tree"));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return transformFolderTree(data.tree || {});
|
return transformFolderTree(data.tree || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -12334,7 +12341,7 @@ function useLoraPoolApi() {
|
|||||||
if (params.namePatternsUseRegex !== void 0) {
|
if (params.namePatternsUseRegex !== void 0) {
|
||||||
urlParams.set("name_pattern_use_regex", String(params.namePatternsUseRegex));
|
urlParams.set("name_pattern_use_regex", String(params.namePatternsUseRegex));
|
||||||
}
|
}
|
||||||
const response = await fetch(`/api/lm/loras/list?${urlParams}`);
|
const response = await fetch(lmApiUrl(`/api/lm/loras/list?${urlParams}`));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return {
|
return {
|
||||||
items: data.items || [],
|
items: data.items || [],
|
||||||
@@ -12682,7 +12689,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
const previewUrls = ref({});
|
const previewUrls = ref({});
|
||||||
const fetchPreviewUrl = async (loraName) => {
|
const fetchPreviewUrl = async (loraName) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/lm/loras/preview-url?name=${encodeURIComponent(loraName)}`);
|
const response = await fetch(lmApiUrl(`/api/lm/loras/preview-url?name=${encodeURIComponent(loraName)}`));
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.preview_url) {
|
if (data.preview_url) {
|
||||||
@@ -12732,7 +12739,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LastUsedPreview = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-b940502e"]]);
|
const LastUsedPreview = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-7db61bc9"]]);
|
||||||
const _hoisted_1$7 = { class: "slider-handle__value" };
|
const _hoisted_1$7 = { class: "slider-handle__value" };
|
||||||
const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
||||||
__name: "SingleSlider",
|
__name: "SingleSlider",
|
||||||
@@ -13528,7 +13535,7 @@ function useLoraRandomizerState(widget) {
|
|||||||
if (poolConfig) {
|
if (poolConfig) {
|
||||||
requestBody.pool_config = poolConfig.filters || {};
|
requestBody.pool_config = poolConfig.filters || {};
|
||||||
}
|
}
|
||||||
const response = await fetch("/api/lm/loras/random-sample", {
|
const response = await fetch(lmApiUrl("/api/lm/loras/random-sample"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -14238,7 +14245,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
const customPreviewUrlResolver = async (modelName) => {
|
const customPreviewUrlResolver = async (modelName) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/lm/loras/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`
|
lmApiUrl(`/api/lm/loras/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`)
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch preview URL");
|
throw new Error("Failed to fetch preview URL");
|
||||||
@@ -14364,7 +14371,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LoraListModal = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["__scopeId", "data-v-83f6f852"]]);
|
const LoraListModal = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["__scopeId", "data-v-ed10780a"]]);
|
||||||
function useLoraCyclerState(widget) {
|
function useLoraCyclerState(widget) {
|
||||||
let isRestoring = false;
|
let isRestoring = false;
|
||||||
const currentIndex = ref(1);
|
const currentIndex = ref(1);
|
||||||
@@ -14491,7 +14498,7 @@ function useLoraCyclerState(widget) {
|
|||||||
if (poolConfig == null ? void 0 : poolConfig.filters) {
|
if (poolConfig == null ? void 0 : poolConfig.filters) {
|
||||||
requestBody.pool_config = poolConfig.filters;
|
requestBody.pool_config = poolConfig.filters;
|
||||||
}
|
}
|
||||||
const response = await fetch("/api/lm/loras/cycler-list", {
|
const response = await fetch(lmApiUrl("/api/lm/loras/cycler-list"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -15898,6 +15905,13 @@ function stripAutocompleteMetadataFromPromptResult(result) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function getComfyUIBasePath() {
|
||||||
|
const { pathname } = window.location;
|
||||||
|
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||||
|
}
|
||||||
|
function lmUrl(path) {
|
||||||
|
return `${getComfyUIBasePath()}${path}`;
|
||||||
|
}
|
||||||
const ROOT_GRAPH_ID = "root";
|
const ROOT_GRAPH_ID = "root";
|
||||||
const LORA_PROVIDER_NODE_TYPES = [
|
const LORA_PROVIDER_NODE_TYPES = [
|
||||||
"Lora Stacker (LoraManager)",
|
"Lora Stacker (LoraManager)",
|
||||||
@@ -16055,7 +16069,7 @@ function updateConnectedTriggerWords(node, loraNames) {
|
|||||||
if (nodeIds.length === 0) {
|
if (nodeIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch("/api/lm/loras/get_trigger_words", {
|
fetch(lmUrl("/api/lm/loras/get_trigger_words"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import { lmUrl } from "./base_path.js";
|
||||||
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, getChildGraphs, chainCallback, getLinkFromGraph } from "./utils.js";
|
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, getChildGraphs, chainCallback, getLinkFromGraph } from "./utils.js";
|
||||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
|
||||||
@@ -396,7 +397,7 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
this._lastFingerprint = fingerprint;
|
this._lastFingerprint = fingerprint;
|
||||||
|
|
||||||
const response = await fetch("/api/lm/register-nodes", {
|
const response = await fetch(lmUrl("/api/lm/register-nodes"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
Reference in New Issue
Block a user