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

@@ -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);