feat(i18n): add license and content usage filter labels

Add new translation keys for model filter interface:
- license
- noCreditRequired
- allowSellingGeneratedContent

These labels support new filtering options for model licensing and content usage permissions, enabling users to filter models based on their license requirements and commercial usage rights.
This commit is contained in:
Will Miao
2025-11-08 10:20:28 +08:00
parent 3b842355c2
commit 1f627774c1
15 changed files with 176 additions and 5 deletions

View File

@@ -188,6 +188,9 @@
"title": "Modelle filtern",
"baseModel": "Basis-Modell",
"modelTags": "Tags (Top 20)",
"license": "Lizenz",
"noCreditRequired": "Kein Credit erforderlich",
"allowSellingGeneratedContent": "Verkauf erlaubt",
"clearAll": "Alle Filter löschen"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "Filter Models",
"baseModel": "Base Model",
"modelTags": "Tags (Top 20)",
"license": "License",
"noCreditRequired": "No Credit Required",
"allowSellingGeneratedContent": "Allow Selling",
"clearAll": "Clear All Filters"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "Filtrar modelos",
"baseModel": "Modelo base",
"modelTags": "Etiquetas (Top 20)",
"license": "Licencia",
"noCreditRequired": "Sin crédito requerido",
"allowSellingGeneratedContent": "Venta permitida",
"clearAll": "Limpiar todos los filtros"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "Filtrer les modèles",
"baseModel": "Modèle de base",
"modelTags": "Tags (Top 20)",
"license": "Licence",
"noCreditRequired": "Crédit non requis",
"allowSellingGeneratedContent": "Vente autorisée",
"clearAll": "Effacer tous les filtres"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "סנן מודלים",
"baseModel": "מודל בסיס",
"modelTags": "תגיות (20 המובילות)",
"license": "רישיון",
"noCreditRequired": "ללא קרדיט נדרש",
"allowSellingGeneratedContent": "אפשר מכירה",
"clearAll": "נקה את כל המסננים"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "モデルをフィルタ",
"baseModel": "ベースモデル",
"modelTags": "タグ上位20",
"license": "ライセンス",
"noCreditRequired": "クレジット不要",
"allowSellingGeneratedContent": "販売許可",
"clearAll": "すべてのフィルタをクリア"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "모델 필터",
"baseModel": "베이스 모델",
"modelTags": "태그 (상위 20개)",
"license": "라이선스",
"noCreditRequired": "크레딧 표기 없음",
"allowSellingGeneratedContent": "판매 허용",
"clearAll": "모든 필터 지우기"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "Фильтр моделей",
"baseModel": "Базовая модель",
"modelTags": "Теги (Топ 20)",
"license": "Лицензия",
"noCreditRequired": "Без указания авторства",
"allowSellingGeneratedContent": "Продажа разрешена",
"clearAll": "Очистить все фильтры"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "筛选模型",
"baseModel": "基础模型",
"modelTags": "标签前20",
"license": "许可证",
"noCreditRequired": "无需署名",
"allowSellingGeneratedContent": "允许销售",
"clearAll": "清除所有筛选"
},
"theme": {

View File

@@ -188,6 +188,9 @@
"title": "篩選模型",
"baseModel": "基礎模型",
"modelTags": "標籤(前 20",
"license": "授權",
"noCreditRequired": "無需署名",
"allowSellingGeneratedContent": "允許銷售",
"clearAll": "清除所有篩選"
},
"theme": {

View File

@@ -51,6 +51,8 @@ html, body {
--lora-border: oklch(72% 0.03 256 / 0.45);
--lora-text: oklch(95% 0.02 256);
--lora-error: oklch(75% 0.32 29);
--lora-error-bg: color-mix(in oklch, var(--lora-error) 20%, transparent);
--lora-error-border: color-mix(in oklch, var(--lora-error) 50%, transparent);
--lora-warning: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
--lora-success: oklch(var(--lora-success-l) var(--lora-success-c) var(--lora-success-h));
--badge-update-bg: oklch(72% 0.2 220);
@@ -103,6 +105,8 @@ html[data-theme="light"] {
--lora-border: oklch(90% 0.02 256 / 0.15);
--lora-text: oklch(98% 0.02 256);
--lora-warning: oklch(75% 0.25 80); /* Modified to be used with oklch() */
--lora-error-bg: color-mix(in oklch, var(--lora-error) 15%, transparent);
--lora-error-border: color-mix(in oklch, var(--lora-error) 40%, transparent);
--badge-update-bg: oklch(62% 0.18 220);
--badge-update-text: oklch(98% 0.02 240);
--badge-update-glow: oklch(62% 0.18 220 / 0.4);

View File

@@ -235,6 +235,13 @@
border-color: var(--lora-accent);
}
/* Exclude state styling for filter tags */
.filter-tag.exclude {
background-color: var(--lora-error-bg);
color: var(--lora-error);
border-color: var(--lora-error-border);
}
/* Tag filter styles */
.tag-filter {
display: flex;

View File

@@ -817,6 +817,33 @@ export class BaseModelApiClient {
params.append('base_model', model);
});
}
// Add license filters
if (pageState.filters.license) {
const licenseFilters = pageState.filters.license;
if (licenseFilters.noCredit) {
// For noCredit filter:
// - 'include' means credit_required=False (no credit required)
// - 'exclude' means credit_required=True (credit required)
if (licenseFilters.noCredit === 'include') {
params.append('credit_required', 'false');
} else if (licenseFilters.noCredit === 'exclude') {
params.append('credit_required', 'true');
}
}
if (licenseFilters.allowSelling) {
// For allowSelling filter:
// - 'include' means allow_selling_generated_content=True
// - 'exclude' means allow_selling_generated_content=False
if (licenseFilters.allowSelling === 'include') {
params.append('allow_selling_generated_content', 'true');
} else if (licenseFilters.allowSelling === 'exclude') {
params.append('allow_selling_generated_content', 'false');
}
}
}
}
this._addModelSpecificParams(params, pageState);

View File

@@ -14,7 +14,8 @@ export class FilterManager {
this.filters = pageState.filters || {
baseModel: [],
tags: []
tags: [],
license: {}
};
this.filterPanel = document.getElementById('filterPanel');
@@ -36,6 +37,9 @@ export class FilterManager {
this.createBaseModelTags();
}
// Add click handlers for license filter tags
this.initializeLicenseFilters();
// Add click handler for filter button
if (this.filterButton) {
this.filterButton.addEventListener('click', () => {
@@ -129,6 +133,85 @@ export class FilterManager {
});
}
initializeLicenseFilters() {
const licenseTags = document.querySelectorAll('.license-tag');
licenseTags.forEach(tag => {
tag.addEventListener('click', async () => {
const licenseType = tag.dataset.license;
// Ensure license object exists
if (!this.filters.license) {
this.filters.license = {};
}
// Get current state
let currentState = this.filters.license[licenseType] || 'none'; // none, include, exclude
// Cycle through states: none -> include -> exclude -> none
let newState;
switch (currentState) {
case 'none':
newState = 'include';
tag.classList.remove('exclude');
tag.classList.add('active');
break;
case 'include':
newState = 'exclude';
tag.classList.remove('active');
tag.classList.add('exclude');
break;
case 'exclude':
newState = 'none';
tag.classList.remove('active', 'exclude');
break;
}
// Update filter state
if (newState === 'none') {
delete this.filters.license[licenseType];
// Clean up empty license object
if (Object.keys(this.filters.license).length === 0) {
delete this.filters.license;
}
} else {
this.filters.license[licenseType] = newState;
}
this.updateActiveFiltersCount();
// Auto-apply filter when tag is clicked
await this.applyFilters(false);
});
});
// Update selections based on stored filters
this.updateLicenseSelections();
}
updateLicenseSelections() {
const licenseTags = document.querySelectorAll('.license-tag');
licenseTags.forEach(tag => {
const licenseType = tag.dataset.license;
const state = (this.filters.license && this.filters.license[licenseType]) || 'none';
// Reset classes
tag.classList.remove('active', 'exclude');
// Apply appropriate class based on state
switch (state) {
case 'include':
tag.classList.add('active');
break;
case 'exclude':
tag.classList.add('exclude');
break;
default:
// none state - no classes needed
break;
}
});
}
createBaseModelTags() {
const baseModelTagsContainer = document.getElementById('baseModelTags');
if (!baseModelTagsContainer) return;
@@ -233,10 +316,15 @@ export class FilterManager {
tag.classList.remove('active');
}
});
// Update license tags
this.updateLicenseSelections();
}
updateActiveFiltersCount() {
const totalActiveFilters = this.filters.baseModel.length + this.filters.tags.length;
const totalActiveFilters = this.filters.baseModel.length +
this.filters.tags.length +
(this.filters.license ? Object.keys(this.filters.license).length : 0);
if (this.activeFiltersCount) {
if (totalActiveFilters > 0) {
@@ -296,7 +384,8 @@ export class FilterManager {
// Clear all filters
this.filters = {
baseModel: [],
tags: []
tags: [],
license: {} // Initialize with empty object instead of deleting
};
// Update state
@@ -337,7 +426,8 @@ export class FilterManager {
// Ensure backward compatibility with older filter format
this.filters = {
baseModel: savedFilters.baseModel || [],
tags: savedFilters.tags || []
tags: savedFilters.tags || [],
license: savedFilters.license || {}
};
// Update state with loaded filters
@@ -357,6 +447,8 @@ export class FilterManager {
}
hasActiveFilters() {
return this.filters.baseModel.length > 0 || this.filters.tags.length > 0;
return this.filters.baseModel.length > 0 ||
this.filters.tags.length > 0 ||
(this.filters.license && Object.keys(this.filters.license).length > 0);
}
}

View File

@@ -139,6 +139,17 @@
<div class="tags-loading">{{ t('common.status.loading') }}</div>
</div>
</div>
<div class="filter-section">
<h4>{{ t('header.filter.license') }}</h4>
<div class="filter-tags">
<div class="filter-tag license-tag" data-license="noCredit">
{{ t('header.filter.noCreditRequired') }}
</div>
<div class="filter-tag license-tag" data-license="allowSelling">
{{ t('header.filter.allowSellingGeneratedContent') }}
</div>
</div>
</div>
<div class="filter-actions">
<button class="clear-filters-btn" onclick="filterManager.clearFilters()">
{{ t('header.filter.clearAll') }}