feat: Add prompt search filter for recipes and fix 'Favorites' localization across multiple languages.

This commit is contained in:
Will Miao
2025-12-23 10:52:12 +08:00
parent 61816cf75d
commit bc9dd317f7
16 changed files with 125 additions and 43 deletions

View File

@@ -188,7 +188,8 @@
"creator": "Ersteller", "creator": "Ersteller",
"title": "Rezept-Titel", "title": "Rezept-Titel",
"loraName": "LoRA-Dateiname", "loraName": "LoRA-Dateiname",
"loraModel": "LoRA-Modellname" "loraModel": "LoRA-Modellname",
"prompt": "Prompt"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "Gefiltert nach LoRA", "filteredByLora": "Gefiltert nach LoRA",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "Nur Favoriten anzeigen",
"action": "[TODO: Translate] Favorites" "action": "Favoriten"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "Creator", "creator": "Creator",
"title": "Recipe Title", "title": "Recipe Title",
"loraName": "LoRA Filename", "loraName": "LoRA Filename",
"loraModel": "LoRA Model Name" "loraModel": "LoRA Model Name",
"prompt": "Prompt"
} }
}, },
"filter": { "filter": {

View File

@@ -188,7 +188,8 @@
"creator": "Creador", "creator": "Creador",
"title": "Título de la receta", "title": "Título de la receta",
"loraName": "Nombre de archivo LoRA", "loraName": "Nombre de archivo LoRA",
"loraModel": "Nombre del modelo LoRA" "loraModel": "Nombre del modelo LoRA",
"prompt": "Prompt"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "Filtrado por LoRA", "filteredByLora": "Filtrado por LoRA",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "Mostrar solo favoritos",
"action": "[TODO: Translate] Favorites" "action": "Favoritos"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "Créateur", "creator": "Créateur",
"title": "Titre de la recipe", "title": "Titre de la recipe",
"loraName": "Nom de fichier LoRA", "loraName": "Nom de fichier LoRA",
"loraModel": "Nom du modèle LoRA" "loraModel": "Nom du modèle LoRA",
"prompt": "Prompt"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "Filtré par LoRA", "filteredByLora": "Filtré par LoRA",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "Afficher uniquement les favoris",
"action": "[TODO: Translate] Favorites" "action": "Favoris"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "יוצר", "creator": "יוצר",
"title": "כותרת מתכון", "title": "כותרת מתכון",
"loraName": "שם קובץ LoRA", "loraName": "שם קובץ LoRA",
"loraModel": "שם מודל LoRA" "loraModel": "שם מודל LoRA",
"prompt": "הנחיה"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "מסונן לפי LoRA", "filteredByLora": "מסונן לפי LoRA",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "הצג מועדפים בלבד",
"action": "[TODO: Translate] Favorites" "action": "מועדפים"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "作成者", "creator": "作成者",
"title": "レシピタイトル", "title": "レシピタイトル",
"loraName": "LoRAファイル名", "loraName": "LoRAファイル名",
"loraModel": "LoRAモデル名" "loraModel": "LoRAモデル名",
"prompt": "プロンプト"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "LoRAでフィルタ済み", "filteredByLora": "LoRAでフィルタ済み",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "お気に入りのみ表示",
"action": "[TODO: Translate] Favorites" "action": "お気に入り"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "제작자", "creator": "제작자",
"title": "레시피 제목", "title": "레시피 제목",
"loraName": "LoRA 파일명", "loraName": "LoRA 파일명",
"loraModel": "LoRA 모델명" "loraModel": "LoRA 모델명",
"prompt": "프롬프트"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "LoRA로 필터링됨", "filteredByLora": "LoRA로 필터링됨",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "즐겨찾기만 표시",
"action": "[TODO: Translate] Favorites" "action": "즐겨찾기"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "Автор", "creator": "Автор",
"title": "Название рецепта", "title": "Название рецепта",
"loraName": "Имя файла LoRA", "loraName": "Имя файла LoRA",
"loraModel": "Название модели LoRA" "loraModel": "Название модели LoRA",
"prompt": "Запрос"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "Фильтр по LoRA", "filteredByLora": "Фильтр по LoRA",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "Только избранные",
"action": "[TODO: Translate] Favorites" "action": "Избранное"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -188,7 +188,8 @@
"creator": "创作者", "creator": "创作者",
"title": "配方标题", "title": "配方标题",
"loraName": "LoRA 文件名", "loraName": "LoRA 文件名",
"loraModel": "LoRA 模型名称" "loraModel": "LoRA 模型名称",
"prompt": "提示词"
} }
}, },
"filter": { "filter": {
@@ -1484,4 +1485,4 @@
"learnMore": "浏览器插件教程" "learnMore": "浏览器插件教程"
} }
} }
} }

View File

@@ -188,7 +188,8 @@
"creator": "創作者", "creator": "創作者",
"title": "配方標題", "title": "配方標題",
"loraName": "LoRA 檔案名稱", "loraName": "LoRA 檔案名稱",
"loraModel": "LoRA 模型名稱" "loraModel": "LoRA 模型名稱",
"prompt": "提示詞"
} }
}, },
"filter": { "filter": {
@@ -593,8 +594,8 @@
}, },
"filteredByLora": "已依 LoRA 篩選", "filteredByLora": "已依 LoRA 篩選",
"favorites": { "favorites": {
"title": "[TODO: Translate] Show Favorites Only", "title": "僅顯示收藏",
"action": "[TODO: Translate] Favorites" "action": "收藏"
} }
}, },
"duplicates": { "duplicates": {

View File

@@ -163,6 +163,7 @@ class RecipeListingHandler:
"tags": request.query.get("search_tags", "true").lower() == "true", "tags": request.query.get("search_tags", "true").lower() == "true",
"lora_name": request.query.get("search_lora_name", "true").lower() == "true", "lora_name": request.query.get("search_lora_name", "true").lower() == "true",
"lora_model": request.query.get("search_lora_model", "true").lower() == "true", "lora_model": request.query.get("search_lora_model", "true").lower() == "true",
"prompt": request.query.get("search_prompt", "true").lower() == "true",
} }
filters: Dict[str, Any] = {} filters: Dict[str, Any] = {}

View File

@@ -1107,6 +1107,14 @@ class RecipeScanner:
if fuzzy_match(str(lora.get('modelName', '')), search): if fuzzy_match(str(lora.get('modelName', '')), search):
return True return True
# Search in prompt and negative_prompt if enabled
if search_options.get('prompt', True) and 'gen_params' in item:
gen_params = item['gen_params']
if fuzzy_match(str(gen_params.get('prompt', '')), search):
return True
if fuzzy_match(str(gen_params.get('negative_prompt', '')), search):
return True
# No match found # No match found
return False return False

View File

@@ -96,6 +96,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
params.append('search_tags', pageState.searchOptions.tags.toString()); params.append('search_tags', pageState.searchOptions.tags.toString());
params.append('search_lora_name', pageState.searchOptions.loraName.toString()); params.append('search_lora_name', pageState.searchOptions.loraName.toString());
params.append('search_lora_model', pageState.searchOptions.loraModel.toString()); params.append('search_lora_model', pageState.searchOptions.loraModel.toString());
params.append('search_prompt', (pageState.searchOptions.prompt || false).toString());
params.append('fuzzy', 'true'); params.append('fuzzy', 'true');
} }
} }

View File

@@ -106,6 +106,7 @@ class RecipeManager {
tags: true, // Recipe tags tags: true, // Recipe tags
loraName: true, // LoRA file name loraName: true, // LoRA file name
loraModel: true, // LoRA model name loraModel: true, // LoRA model name
prompt: true, // Prompt search
recursive: true recursive: true
}; };
} }

View File

@@ -8,52 +8,60 @@
</div> </div>
{% set current_path = request.path %} {% set current_path = request.path %}
{% if current_path.startswith('/loras/recipes') %} {% if current_path.startswith('/loras/recipes') %}
{% set current_page = 'recipes' %} {% set current_page = 'recipes' %}
{% elif current_path.startswith('/checkpoints') %} {% elif current_path.startswith('/checkpoints') %}
{% set current_page = 'checkpoints' %} {% set current_page = 'checkpoints' %}
{% elif current_path.startswith('/embeddings') %} {% elif current_path.startswith('/embeddings') %}
{% set current_page = 'embeddings' %} {% set current_page = 'embeddings' %}
{% elif current_path.startswith('/statistics') %} {% elif current_path.startswith('/statistics') %}
{% set current_page = 'statistics' %} {% set current_page = 'statistics' %}
{% else %} {% else %}
{% set current_page = 'loras' %} {% set current_page = 'loras' %}
{% endif %} {% endif %}
{% set search_disabled = current_page == 'statistics' %} {% set search_disabled = current_page == 'statistics' %}
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~ current_page %} {% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
current_page %}
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %} {% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
<nav class="main-nav"> <nav class="main-nav">
<a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem"> <a href="/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 %}" id="recipesNavItem"> <a href="/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> <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 %}" id="checkpointsNavItem"> <a href="/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> <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 %}" id="embeddingsNavItem"> <a href="/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> <i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
</a> </a>
<a href="/statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}" id="statisticsNavItem"> <a href="/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> <i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
</a> </a>
</nav> </nav>
<!-- Context-aware search container --> <!-- Context-aware search container -->
<div class="{{ header_search_class }}" id="headerSearch"> <div class="{{ header_search_class }}" id="headerSearch">
<div class="search-container"> <div class="search-container">
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}"{% if search_disabled %} disabled{% endif %} /> <input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
disabled{% endif %} />
<i class="fas fa-search search-icon"></i> <i class="fas fa-search search-icon"></i>
<button class="search-options-toggle" id="searchOptionsToggle" title="{{ t('header.search.options') }}"{% if search_disabled %} disabled aria-disabled="true"{% endif %}> <button class="search-options-toggle" id="searchOptionsToggle" title="{{ t('header.search.options') }}" {% if
search_disabled %} disabled aria-disabled="true" {% endif %}>
<i class="fas fa-sliders-h"></i> <i class="fas fa-sliders-h"></i>
</button> </button>
<button class="search-filter-toggle" id="filterButton" title="{{ t('header.filter.title') }}"{% if search_disabled %} disabled aria-disabled="true"{% endif %}> <button class="search-filter-toggle" id="filterButton" title="{{ t('header.filter.title') }}" {% if
search_disabled %} disabled aria-disabled="true" {% endif %}>
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
<span class="filter-badge" id="activeFiltersCount" style="display: none">0</span> <span class="filter-badge" id="activeFiltersCount" style="display: none">0</span>
</button> </button>
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<!-- Integrated corner controls --> <!-- Integrated corner controls -->
<div class="header-controls"> <div class="header-controls">
@@ -97,6 +105,7 @@
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div> <div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
<div class="search-option-tag active" data-option="loraName">{{ t('header.search.filters.loraName') }}</div> <div class="search-option-tag active" data-option="loraName">{{ t('header.search.filters.loraName') }}</div>
<div class="search-option-tag active" data-option="loraModel">{{ t('header.search.filters.loraModel') }}</div> <div class="search-option-tag active" data-option="loraModel">{{ t('header.search.filters.loraModel') }}</div>
<div class="search-option-tag active" data-option="prompt">{{ t('header.search.filters.prompt') }}</div>
{% elif request.path == '/checkpoints' %} {% elif request.path == '/checkpoints' %}
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div> <div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div> <div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
@@ -165,4 +174,4 @@
{{ t('header.filter.clearAll') }} {{ t('header.filter.clearAll') }}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -549,3 +549,55 @@ async def test_get_paginated_data_filters_by_favorite(recipe_scanner):
result_fav_false = await scanner.get_paginated_data(page=1, page_size=10, filters={"favorite": False}) result_fav_false = await scanner.get_paginated_data(page=1, page_size=10, filters={"favorite": False})
assert len(result_fav_false["items"]) == 2 assert len(result_fav_false["items"]) == 2
@pytest.mark.asyncio
async def test_get_paginated_data_filters_by_prompt(recipe_scanner):
scanner, _ = recipe_scanner
# Add a recipe with a specific prompt
await scanner.add_recipe({
"id": "prompt-recipe",
"file_path": "path/prompt.png",
"title": "Prompt Recipe",
"modified": 1.0,
"created_date": 1.0,
"loras": [],
"gen_params": {
"prompt": "a beautiful forest landscape"
}
})
# Add a recipe with a specific negative prompt
await scanner.add_recipe({
"id": "neg-prompt-recipe",
"file_path": "path/neg.png",
"title": "Negative Prompt Recipe",
"modified": 2.0,
"created_date": 2.0,
"loras": [],
"gen_params": {
"negative_prompt": "ugly, blurry mountains"
}
})
await asyncio.sleep(0)
# Test search in prompt
result_prompt = await scanner.get_paginated_data(
page=1, page_size=10, search="forest", search_options={"prompt": True}
)
assert len(result_prompt["items"]) == 1
assert result_prompt["items"][0]["id"] == "prompt-recipe"
# Test search in negative prompt
result_neg = await scanner.get_paginated_data(
page=1, page_size=10, search="mountains", search_options={"prompt": True}
)
assert len(result_neg["items"]) == 1
assert result_neg["items"][0]["id"] == "neg-prompt-recipe"
# Test search disabled (should not find by prompt)
result_disabled = await scanner.get_paginated_data(
page=1, page_size=10, search="forest", search_options={"prompt": False}
)
assert len(result_disabled["items"]) == 0