feat: add license flags display to model preview tooltip #613

- Add optional license_flags parameter to model preview API endpoint
- Include license flags in response when requested via query parameter
- Add CSS styles for license overlay and icons in tooltip
- Implement license flag parsing and icon mapping logic
- Display license restrictions as icons in preview tooltip overlay

This allows users to see model license restrictions directly in the preview tooltip without needing to navigate to detailed model information pages.
This commit is contained in:
Will Miao
2025-11-08 19:08:55 +08:00
parent 2e6aa5fe9f
commit 5ee93a27ee
3 changed files with 131 additions and 4 deletions

View File

@@ -659,9 +659,16 @@ class ModelQueryHandler:
model_name = request.query.get("name")
if not model_name:
return web.Response(text=f"{self._service.model_type.capitalize()} file name is required", status=400)
include_license_flags = (request.query.get("license_flags", "").strip().lower() in {"1", "true", "yes", "on"})
preview_url = await self._service.get_model_preview_url(model_name)
if preview_url:
return web.json_response({"success": True, "preview_url": preview_url})
response_payload: dict[str, object] = {"success": True, "preview_url": preview_url}
if include_license_flags:
model_data = await self._service.get_model_info_by_name(model_name)
license_flags = (model_data or {}).get("license_flags")
if license_flags is not None:
response_payload["license_flags"] = int(license_flags)
return web.json_response(response_payload)
return web.json_response({"success": False, "error": f"No preview URL found for the specified {self._service.model_type}"}, status=404)
except Exception as exc:
self._logger.error("Error getting %s preview URL: %s", self._service.model_type, exc, exc_info=True)

View File

@@ -43,6 +43,32 @@
-webkit-backdrop-filter: blur(4px);
}
.lm-tooltip__license-overlay {
position: absolute;
top: 8px;
left: 8px;
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 4px 8px;
border-radius: 999px;
background: rgba(10, 10, 14, 0.78);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
max-width: calc(100% - 16px);
}
.lm-tooltip__license-icon {
width: 18px;
height: 18px;
display: inline-block;
background-color: rgba(255, 255, 255, 0.9);
-webkit-mask: var(--license-icon-image) center/contain no-repeat;
mask: var(--license-icon-image) center/contain no-repeat;
}
.lm-loras-container {
display: flex;
flex-direction: column;

View File

@@ -1,6 +1,83 @@
import { api } from "../../scripts/api.js";
import { ensureLmStyles } from "./lm_styles_loader.js";
const LICENSE_ICON_PATH = "/loras_static/images/tabler/";
const LICENSE_FLAG_BITS = {
allowNoCredit: 1 << 0,
allowOnImages: 1 << 1,
allowOnCivitai: 1 << 2,
allowRental: 1 << 3,
allowSellingModels: 1 << 4,
allowDerivatives: 1 << 5,
allowRelicense: 1 << 6,
};
const LICENSE_ICON_COPY = {
credit: "Creator credit required",
image: "No selling generated content",
rentcivit: "No Civitai generation",
rent: "No generation services",
sell: "No selling models",
derivatives: "No sharing merges",
relicense: "Same permissions required",
};
const COMMERCIAL_ICON_CONFIG = [
{ bit: LICENSE_FLAG_BITS.allowOnImages, icon: "photo-off.svg", label: LICENSE_ICON_COPY.image },
{ bit: LICENSE_FLAG_BITS.allowOnCivitai, icon: "brush-off.svg", label: LICENSE_ICON_COPY.rentcivit },
{ bit: LICENSE_FLAG_BITS.allowRental, icon: "world-off.svg", label: LICENSE_ICON_COPY.rent },
{ bit: LICENSE_FLAG_BITS.allowSellingModels, icon: "shopping-cart-off.svg", label: LICENSE_ICON_COPY.sell },
];
function parseLicenseFlags(value) {
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}
function buildLicenseIconData(licenseFlags) {
if (licenseFlags == null) {
return [];
}
const icons = [];
if ((licenseFlags & LICENSE_FLAG_BITS.allowNoCredit) === 0) {
icons.push({ icon: "user-check.svg", label: LICENSE_ICON_COPY.credit });
}
COMMERCIAL_ICON_CONFIG.forEach((config) => {
if ((licenseFlags & config.bit) === 0) {
icons.push({ icon: config.icon, label: config.label });
}
});
if ((licenseFlags & LICENSE_FLAG_BITS.allowDerivatives) === 0) {
icons.push({ icon: "exchange-off.svg", label: LICENSE_ICON_COPY.derivatives });
}
if ((licenseFlags & LICENSE_FLAG_BITS.allowRelicense) === 0) {
icons.push({ icon: "rotate-2.svg", label: LICENSE_ICON_COPY.relicense });
}
return icons;
}
function createLicenseIconElement({ icon, label }) {
const element = document.createElement("span");
element.className = "lm-tooltip__license-icon";
element.setAttribute("role", "img");
element.setAttribute("aria-label", label);
element.title = label;
element.style.setProperty("--license-icon-image", `url('${LICENSE_ICON_PATH}${icon}')`);
return element;
}
/**
* Lightweight preview tooltip that can display images or videos for different model types.
*/
@@ -44,7 +121,7 @@ export class PreviewTooltip {
async defaultPreviewUrlResolver(modelName) {
const response = await api.fetchApi(
`/lm/${this.modelType}/preview-url?name=${encodeURIComponent(modelName)}`,
`/lm/${this.modelType}/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`,
{ method: "GET" }
);
if (!response.ok) {
@@ -57,6 +134,7 @@ export class PreviewTooltip {
return {
previewUrl: data.preview_url,
displayName: data.display_name ?? modelName,
licenseFlags: parseLicenseFlags(data.license_flags),
};
}
@@ -72,7 +150,7 @@ export class PreviewTooltip {
};
}
const { previewUrl, displayName } = raw;
const { previewUrl, displayName, licenseFlags } = raw;
if (!previewUrl) {
throw new Error("No preview URL available");
}
@@ -82,6 +160,7 @@ export class PreviewTooltip {
displayName !== undefined
? displayName
: this.displayNameFormatter(modelName),
licenseFlags: parseLicenseFlags(licenseFlags),
};
}
@@ -103,7 +182,7 @@ export class PreviewTooltip {
}
this.currentModelName = modelName;
const { previewUrl, displayName } = await this.resolvePreviewData(
const { previewUrl, displayName, licenseFlags } = await this.resolvePreviewData(
modelName
);
@@ -132,6 +211,7 @@ export class PreviewTooltip {
nameLabel.className = "lm-tooltip__label";
mediaContainer.appendChild(mediaElement);
this.renderLicenseOverlay(mediaContainer, licenseFlags);
mediaContainer.appendChild(nameLabel);
this.element.appendChild(mediaContainer);
@@ -213,6 +293,20 @@ export class PreviewTooltip {
}
}
renderLicenseOverlay(container, licenseFlags) {
const icons = buildLicenseIconData(licenseFlags);
if (!icons.length) {
return;
}
const overlay = document.createElement("div");
overlay.className = "lm-tooltip__license-overlay";
icons.forEach((descriptor) => {
overlay.appendChild(createLicenseIconElement(descriptor));
});
container.appendChild(overlay);
}
cleanup() {
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);