mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-24 04:24:09 -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.file_utils import calculate_sha256
|
||||
from ...utils.metadata_manager import MetadataManager
|
||||
from ...utils.url_utils import relative_root_prefix
|
||||
|
||||
LICENSE_FIELDS = (
|
||||
"allowNoCredit",
|
||||
@@ -204,6 +205,7 @@ class ModelPageView:
|
||||
"version": self._get_app_version(),
|
||||
"provider_presets_json": json.dumps(PROVIDER_PRESETS),
|
||||
"provider_models_json": "{}",
|
||||
"rel_prefix": relative_root_prefix(request.path),
|
||||
}
|
||||
|
||||
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.exif_utils import ExifUtils
|
||||
from ...utils.recipe_open_stats import RecipeOpenStats
|
||||
from ...utils.url_utils import relative_root_prefix
|
||||
from ...recipes.merger import GenParamsMerger
|
||||
from ...recipes.enrichment import RecipeEnricher
|
||||
from ...services.websocket_manager import ws_manager as default_ws_manager
|
||||
@@ -215,6 +216,7 @@ class RecipePageView:
|
||||
settings=self._settings,
|
||||
request=request,
|
||||
t=self._server_i18n.get_translation,
|
||||
rel_prefix=relative_root_prefix(request.path),
|
||||
)
|
||||
except Exception as cache_error: # pragma: no cover - logging path
|
||||
self._logger.error("Error loading recipe cache data: %s", cache_error)
|
||||
@@ -223,6 +225,7 @@ class RecipePageView:
|
||||
settings=self._settings,
|
||||
request=request,
|
||||
t=self._server_i18n.get_translation,
|
||||
rel_prefix=relative_root_prefix(request.path),
|
||||
)
|
||||
return web.Response(text=rendered, content_type="text/html")
|
||||
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.model_query import normalize_sub_type, resolve_sub_type
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -106,6 +107,7 @@ class StatsRoutes:
|
||||
settings=settings_manager,
|
||||
request=request,
|
||||
t=server_i18n.get_translation,
|
||||
rel_prefix=relative_root_prefix(request.path),
|
||||
)
|
||||
|
||||
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 { showRematchSummary } from '../RematchSummaryModal.js';
|
||||
import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js';
|
||||
import { withBasePath } from '../../utils/basePath.js';
|
||||
|
||||
export class RecipeContextMenu extends BaseContextMenu {
|
||||
constructor() {
|
||||
@@ -180,7 +181,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
setSessionItem('filterRecipeName', recipe.title);
|
||||
|
||||
// Navigate to the LoRAs page
|
||||
window.location.href = '/loras';
|
||||
window.location.href = withBasePath('/loras');
|
||||
} else {
|
||||
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 { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow, extractRecipeId } from '../api/recipeApi.js';
|
||||
import { downloadManager } from '../managers/DownloadManager.js';
|
||||
import { withBasePath } from '../utils/basePath.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { openMediaViewer } from './shared/MediaViewer.js';
|
||||
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
|
||||
@@ -3231,7 +3232,7 @@ class RecipeModal {
|
||||
setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title);
|
||||
}
|
||||
|
||||
window.location.href = '/checkpoints';
|
||||
window.location.href = withBasePath('/checkpoints');
|
||||
}
|
||||
|
||||
_getCheckpointHash(checkpoint) {
|
||||
@@ -3281,7 +3282,7 @@ class RecipeModal {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { showToast, copyToClipboard } from '../../utils/uiHelpers.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.
|
||||
@@ -356,7 +357,7 @@ function navigateToRecipesPage({ modelKind, displayName, modelHash }) {
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Directly navigate to recipes page
|
||||
window.location.href = '/loras/recipes';
|
||||
window.location.href = withBasePath('/loras/recipes');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<head>
|
||||
{% include 'components/base_path_bootstrap.html' %}
|
||||
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
||||
<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="/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/css/style.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/onboarding.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/vendor/flag-icons/flag-icons.min.css">
|
||||
{% 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">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
||||
<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="{{ rel_prefix }}loras_static/images/favicon-16x16.png">
|
||||
<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>
|
||||
@@ -102,7 +103,7 @@
|
||||
|
||||
{% if is_initializing %}
|
||||
<!-- 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 %}
|
||||
{% block main_script %}{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
@@ -75,5 +75,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -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 -->
|
||||
<div class="header-left">
|
||||
<div class="header-branding">
|
||||
<a href="/loras" class="logo-link">
|
||||
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
||||
<a href="{{ rel_prefix }}loras" class="logo-link">
|
||||
<img src="{{ rel_prefix }}loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
||||
<span class="app-title">{{ t('header.appTitle') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -23,26 +23,26 @@
|
||||
{% set current_page = 'loras' %}
|
||||
{% endif %}
|
||||
<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>
|
||||
</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">
|
||||
<i class="fas fa-book-open"></i> <span>{{ t('header.navigation.recipes') }}</span>
|
||||
</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">
|
||||
<i class="fas fa-check-circle"></i> <span>{{ t('header.navigation.checkpoints') }}</span>
|
||||
</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">
|
||||
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
|
||||
</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">
|
||||
<i class="fas fa-shapes"></i> <span>{{ t('header.navigation.other') }}</span>
|
||||
</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">
|
||||
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
||||
</a>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="tip-carousel" id="tipCarousel">
|
||||
<div class="tip-item active">
|
||||
<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'">
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<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'">
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<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'">
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
@@ -59,7 +59,7 @@
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<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'">
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<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'">
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<h3>{{ t('help.gettingStarted.title') }}</h3>
|
||||
<div class="video-container">
|
||||
<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">
|
||||
<a href="https://www.youtube.com/watch?v=hvKw31YpE-U" target="_blank" class="external-link-btn">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="video-item">
|
||||
<div class="video-container">
|
||||
<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">
|
||||
<a href="https://www.youtube.com/playlist?list=PLU2fMdHNl8ohz1u7Ke3ooOuMbU5Y4sgoj" target="_blank" class="external-link-btn">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<i class="fas fa-chevron-down toggle-icon"></i>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -71,5 +71,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
|
||||
{% block main_script %}
|
||||
{% 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 %}
|
||||
{% endblock %}
|
||||
@@ -167,8 +167,8 @@
|
||||
|
||||
{% block main_script %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
{% block page_id %}recipes{% endblock %}
|
||||
|
||||
{% block page_css %}
|
||||
<link rel="stylesheet" href="/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="/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/card.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/recipe-modal.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/import-modal.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="{{ rel_prefix }}loras_static/css/components/batch-import-modal.css?v={{ version }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_components %}
|
||||
@@ -113,5 +113,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{% block head_scripts %}
|
||||
<!-- 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 %}
|
||||
|
||||
{% block init_title %}{{ t('initialization.statistics.title') }}{% endblock %}
|
||||
@@ -192,6 +192,6 @@
|
||||
|
||||
{% block main_script %}
|
||||
{% 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 %}
|
||||
{% 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"
|
||||
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"
|
||||
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 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 ModalWrapper from '../lora-pool/modals/ModalWrapper.vue'
|
||||
import type { LoraItem } from '../../composables/types'
|
||||
import { lmApiUrl } from '@/utils/basePath'
|
||||
|
||||
interface LoraListItem {
|
||||
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
|
||||
const customPreviewUrlResolver = async (modelName: string) => {
|
||||
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) {
|
||||
throw new Error('Failed to fetch preview URL')
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import type { LoraEntry } from '../../composables/types'
|
||||
import { lmApiUrl } from '@/utils/basePath'
|
||||
|
||||
const props = defineProps<{
|
||||
loras: LoraEntry[]
|
||||
@@ -48,7 +49,7 @@ const previewUrls = ref<Record<string, string>>({})
|
||||
// Fetch preview URL for a lora using API
|
||||
const fetchPreviewUrl = async (loraName: string) => {
|
||||
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) {
|
||||
const data = await response.json()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from './types'
|
||||
import { lmApiUrl } from '@/utils/basePath'
|
||||
|
||||
export interface CyclerLoraItem {
|
||||
file_name: string
|
||||
@@ -173,7 +174,7 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ref } from 'vue'
|
||||
import type { BaseModelOption, TagOption, FolderTreeNode, LoraItem } from './types'
|
||||
import { lmApiUrl } from '@/utils/basePath'
|
||||
|
||||
export function useLoraPoolApi() {
|
||||
const isLoading = ref(false)
|
||||
|
||||
const fetchBaseModels = async (limit = 50): Promise<BaseModelOption[]> => {
|
||||
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()
|
||||
return data.base_models || []
|
||||
} catch (error) {
|
||||
@@ -17,7 +18,7 @@ export function useLoraPoolApi() {
|
||||
|
||||
const fetchTags = async (limit = 0): Promise<TagOption[]> => {
|
||||
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()
|
||||
return data.tags || []
|
||||
} catch (error) {
|
||||
@@ -28,7 +29,7 @@ export function useLoraPoolApi() {
|
||||
|
||||
const fetchFolderTree = async (): Promise<FolderTreeNode[]> => {
|
||||
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()
|
||||
return transformFolderTree(data.tree || {})
|
||||
} catch (error) {
|
||||
@@ -102,7 +103,7 @@ export function useLoraPoolApi() {
|
||||
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()
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ComponentWidget, RandomizerConfig, LoraEntry } from './types'
|
||||
import { lmApiUrl } from '@/utils/basePath'
|
||||
|
||||
export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>) {
|
||||
// Flag to prevent infinite loops during config restoration
|
||||
@@ -160,7 +161,7 @@ export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>
|
||||
}
|
||||
|
||||
// Call API endpoint
|
||||
const response = await fetch('/api/lm/loras/random-sample', {
|
||||
const response = await fetch(lmApiUrl('/api/lm/loras/random-sample'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'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 { api } from "../../scripts/api.js";
|
||||
import { lmUrl } from "./base_path.js";
|
||||
|
||||
// Mirrors the backend resolver (get_lora_info_absolute): a ".ckpt"/".pt"
|
||||
// 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://";
|
||||
let ws;
|
||||
try {
|
||||
ws = new WebSocket(`${protocol}${window.location.host}/ws/fetch-progress`);
|
||||
ws = new WebSocket(`${protocol}${window.location.host}${lmUrl("/ws/fetch-progress")}`);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
@@ -474,7 +475,7 @@ export async function saveRecipeDirectly() {
|
||||
}
|
||||
|
||||
// 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'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { api } from "../../scripts/api.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 = {
|
||||
allowNoCredit: 1 << 0,
|
||||
allowOnImages: 1 << 1,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { lmUrl } from "./base_path.js";
|
||||
|
||||
// ============================================================================
|
||||
// Setting IDs and Defaults
|
||||
@@ -55,7 +56,7 @@ const loadWorkflowOptions = async () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/lm/example-workflows");
|
||||
const response = await fetch(lmUrl("/api/lm/example-workflows"));
|
||||
const data = await response.json();
|
||||
if (data.success && data.workflows) {
|
||||
workflowOptionsFull = data.workflows;
|
||||
@@ -81,7 +82,7 @@ const loadTemplateWorkflow = async (templateName) => {
|
||||
const workflow = workflowOptionsFull.find((w) => w.label === templateName);
|
||||
if (workflow && workflow.value) {
|
||||
const workflowResponse = await fetch(
|
||||
`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`
|
||||
lmUrl(`/api/lm/example-workflows/${encodeURIComponent(workflow.value)}`)
|
||||
);
|
||||
const workflowData = await workflowResponse.json();
|
||||
if (workflowData.success && workflowData.workflow) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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 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 openLoraManager = (event) => {
|
||||
const url = `${window.location.origin}${LORA_MANAGER_PATH}`;
|
||||
const url = `${window.location.origin}${lmUrl(LORA_MANAGER_PATH)}`;
|
||||
|
||||
if (event.shiftKey) {
|
||||
window.open(url, "_blank", NEW_WINDOW_FEATURES);
|
||||
@@ -29,7 +30,7 @@ const getComfyUIFrontendVersion = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/system_stats");
|
||||
const response = await fetch(lmUrl("/system_stats"));
|
||||
const data = await response.json();
|
||||
|
||||
if (data?.system?.comfyui_frontend_version) {
|
||||
@@ -80,7 +81,7 @@ const supportsActionBarButtons = async () => {
|
||||
|
||||
const fetchVersionInfo = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/lm/version-info");
|
||||
const response = await fetch(lmUrl("/api/lm/version-info"));
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ComfyUI extension to track model usage statistics
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { lmUrl } from "./base_path.js";
|
||||
import { showToast } from "./utils.js";
|
||||
import { getAutoPathCorrectionPreference, getUsageStatisticsPreference } from "./settings.js";
|
||||
|
||||
@@ -40,7 +41,7 @@ app.registerExtension({
|
||||
async updateUsageStats(promptId) {
|
||||
try {
|
||||
// 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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const CONVERTED_TYPE = 'converted-widget';
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { AutoComplete } from "./autocomplete.js";
|
||||
import { lmUrl } from "./base_path.js";
|
||||
|
||||
const ROOT_GRAPH_ID = "root";
|
||||
|
||||
@@ -408,7 +409,7 @@ export function updateConnectedTriggerWords(node, loraNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/lm/loras/get_trigger_words", {
|
||||
fetch(lmUrl("/api/lm/loras/get_trigger_words"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1105,7 +1105,7 @@ to { transform: rotate(360deg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.last-used-preview[data-v-b940502e] {
|
||||
.last-used-preview[data-v-7db61bc9] {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
@@ -1113,7 +1113,7 @@ to { transform: rotate(360deg);
|
||||
z-index: 100;
|
||||
width: 280px;
|
||||
}
|
||||
.last-used-preview__content[data-v-b940502e] {
|
||||
.last-used-preview__content[data-v-7db61bc9] {
|
||||
background: var(--comfy-menu-bg, #1a1a1a);
|
||||
border: 1px solid var(--border-color, #444);
|
||||
border-radius: 6px;
|
||||
@@ -1123,7 +1123,7 @@ to { transform: rotate(360deg);
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.last-used-preview__item[data-v-b940502e] {
|
||||
.last-used-preview__item[data-v-7db61bc9] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -1131,7 +1131,7 @@ to { transform: rotate(360deg);
|
||||
background: var(--comfy-input-bg, #333);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.last-used-preview__thumb[data-v-b940502e] {
|
||||
.last-used-preview__thumb[data-v-7db61bc9] {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: cover;
|
||||
@@ -1139,37 +1139,37 @@ to { transform: rotate(360deg);
|
||||
flex-shrink: 0;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--fg-color, #fff);
|
||||
opacity: 0.2;
|
||||
}
|
||||
.last-used-preview__thumb--placeholder svg[data-v-b940502e] {
|
||||
.last-used-preview__thumb--placeholder svg[data-v-7db61bc9] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.last-used-preview__info[data-v-b940502e] {
|
||||
.last-used-preview__info[data-v-7db61bc9] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
.last-used-preview__name[data-v-b940502e] {
|
||||
.last-used-preview__name[data-v-7db61bc9] {
|
||||
font-size: 11px;
|
||||
color: var(--fg-color, #fff);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.last-used-preview__strength[data-v-b940502e] {
|
||||
.last-used-preview__strength[data-v-7db61bc9] {
|
||||
font-size: 10px;
|
||||
color: var(--fg-color, #fff);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.last-used-preview__more[data-v-b940502e] {
|
||||
.last-used-preview__more[data-v-7db61bc9] {
|
||||
font-size: 11px;
|
||||
color: var(--fg-color, #fff);
|
||||
opacity: 0.5;
|
||||
@@ -1957,10 +1957,10 @@ to { transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search-container[data-v-83f6f852] {
|
||||
.search-container[data-v-ed10780a] {
|
||||
position: relative;
|
||||
}
|
||||
.search-icon[data-v-83f6f852] {
|
||||
.search-icon[data-v-ed10780a] {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
@@ -1970,7 +1970,7 @@ to { transform: rotate(360deg);
|
||||
color: var(--fg-color, #fff);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.search-input[data-v-83f6f852] {
|
||||
.search-input[data-v-ed10780a] {
|
||||
width: 100%;
|
||||
padding: 8px 32px;
|
||||
background: var(--comfy-input-bg, #333);
|
||||
@@ -1981,14 +1981,14 @@ to { transform: rotate(360deg);
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.search-input[data-v-83f6f852]:focus {
|
||||
.search-input[data-v-ed10780a]:focus {
|
||||
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);
|
||||
opacity: 0.4;
|
||||
}
|
||||
.clear-button[data-v-83f6f852] {
|
||||
.clear-button[data-v-ed10780a] {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
@@ -2005,22 +2005,22 @@ to { transform: rotate(360deg);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.clear-button[data-v-83f6f852]:hover {
|
||||
.clear-button[data-v-ed10780a]:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.clear-button svg[data-v-83f6f852] {
|
||||
.clear-button svg[data-v-ed10780a] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--fg-color, #fff);
|
||||
}
|
||||
.lora-list[data-v-83f6f852] {
|
||||
.lora-list[data-v-ed10780a] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.lora-item[data-v-83f6f852] {
|
||||
.lora-item[data-v-ed10780a] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -2030,14 +2030,14 @@ to { transform: rotate(360deg);
|
||||
transition: all 0.15s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.lora-item[data-v-83f6f852]:hover {
|
||||
.lora-item[data-v-ed10780a]:hover {
|
||||
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);
|
||||
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-size: 12px;
|
||||
color: rgba(226, 232, 240, 0.5);
|
||||
@@ -2045,7 +2045,7 @@ to { transform: rotate(360deg);
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.lora-name[data-v-83f6f852] {
|
||||
.lora-name[data-v-ed10780a] {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--fg-color, #fff);
|
||||
@@ -2053,7 +2053,7 @@ to { transform: rotate(360deg);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.current-badge[data-v-83f6f852] {
|
||||
.current-badge[data-v-ed10780a] {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
background: rgba(66, 153, 225, 0.3);
|
||||
@@ -2062,14 +2062,14 @@ to { transform: rotate(360deg);
|
||||
color: rgba(191, 219, 254, 1);
|
||||
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;
|
||||
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);
|
||||
}
|
||||
.no-results[data-v-83f6f852] {
|
||||
.no-results[data-v-ed10780a] {
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
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"]]);
|
||||
function getLmBasePath() {
|
||||
const { pathname } = window.location;
|
||||
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||
}
|
||||
function lmApiUrl(path) {
|
||||
return `${getLmBasePath()}${path}`;
|
||||
}
|
||||
function useLoraPoolApi() {
|
||||
const isLoading = ref(false);
|
||||
const fetchBaseModels = async (limit = 50) => {
|
||||
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();
|
||||
return data.base_models || [];
|
||||
} catch (error) {
|
||||
@@ -12276,7 +12283,7 @@ function useLoraPoolApi() {
|
||||
};
|
||||
const fetchTags = async (limit = 0) => {
|
||||
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();
|
||||
return data.tags || [];
|
||||
} catch (error) {
|
||||
@@ -12286,7 +12293,7 @@ function useLoraPoolApi() {
|
||||
};
|
||||
const fetchFolderTree = async () => {
|
||||
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();
|
||||
return transformFolderTree(data.tree || {});
|
||||
} catch (error) {
|
||||
@@ -12334,7 +12341,7 @@ function useLoraPoolApi() {
|
||||
if (params.namePatternsUseRegex !== void 0) {
|
||||
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();
|
||||
return {
|
||||
items: data.items || [],
|
||||
@@ -12682,7 +12689,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
||||
const previewUrls = ref({});
|
||||
const fetchPreviewUrl = async (loraName) => {
|
||||
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) {
|
||||
const data = await response.json();
|
||||
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 _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
||||
__name: "SingleSlider",
|
||||
@@ -13528,7 +13535,7 @@ function useLoraRandomizerState(widget) {
|
||||
if (poolConfig) {
|
||||
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",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
@@ -14238,7 +14245,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
||||
};
|
||||
const customPreviewUrlResolver = async (modelName) => {
|
||||
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) {
|
||||
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) {
|
||||
let isRestoring = false;
|
||||
const currentIndex = ref(1);
|
||||
@@ -14491,7 +14498,7 @@ function useLoraCyclerState(widget) {
|
||||
if (poolConfig == null ? void 0 : 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",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
@@ -15898,6 +15905,13 @@ function stripAutocompleteMetadataFromPromptResult(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 LORA_PROVIDER_NODE_TYPES = [
|
||||
"Lora Stacker (LoraManager)",
|
||||
@@ -16055,7 +16069,7 @@ function updateConnectedTriggerWords(node, loraNames) {
|
||||
if (nodeIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
fetch("/api/lm/loras/get_trigger_words", {
|
||||
fetch(lmUrl("/api/lm/loras/get_trigger_words"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
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 { api } from "../../scripts/api.js";
|
||||
import { lmUrl } from "./base_path.js";
|
||||
import { getAllGraphNodes, getNodeReference, getNodeFromGraph, getChildGraphs, chainCallback, getLinkFromGraph } from "./utils.js";
|
||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||
|
||||
@@ -396,7 +397,7 @@ app.registerExtension({
|
||||
}
|
||||
this._lastFingerprint = fingerprint;
|
||||
|
||||
const response = await fetch("/api/lm/register-nodes", {
|
||||
const response = await fetch(lmUrl("/api/lm/register-nodes"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
Reference in New Issue
Block a user