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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -163,6 +163,7 @@ class RecipeListingHandler:
"tags": request.query.get("search_tags", "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",
"prompt": request.query.get("search_prompt", "true").lower() == "true",
}
filters: Dict[str, Any] = {}

View File

@@ -1107,6 +1107,14 @@ class RecipeScanner:
if fuzzy_match(str(lora.get('modelName', '')), search):
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
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_lora_name', pageState.searchOptions.loraName.toString());
params.append('search_lora_model', pageState.searchOptions.loraModel.toString());
params.append('search_prompt', (pageState.searchOptions.prompt || false).toString());
params.append('fuzzy', 'true');
}
}

View File

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

View File

@@ -8,52 +8,60 @@
</div>
{% set current_path = request.path %}
{% if current_path.startswith('/loras/recipes') %}
{% set current_page = 'recipes' %}
{% set current_page = 'recipes' %}
{% elif current_path.startswith('/checkpoints') %}
{% set current_page = 'checkpoints' %}
{% set current_page = 'checkpoints' %}
{% elif current_path.startswith('/embeddings') %}
{% set current_page = 'embeddings' %}
{% set current_page = 'embeddings' %}
{% elif current_path.startswith('/statistics') %}
{% set current_page = 'statistics' %}
{% set current_page = 'statistics' %}
{% else %}
{% set current_page = 'loras' %}
{% set current_page = 'loras' %}
{% endif %}
{% 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' %}
<nav class="main-nav">
<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>
</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>
</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>
</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>
</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>
</a>
</nav>
<!-- Context-aware search container -->
<div class="{{ header_search_class }}" id="headerSearch">
<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>
<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>
</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>
<span class="filter-badge" id="activeFiltersCount" style="display: none">0</span>
</button>
</div>
</div>
<div class="header-actions">
<!-- Integrated corner 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="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="prompt">{{ t('header.search.filters.prompt') }}</div>
{% 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="modelname">{{ t('header.search.filters.modelname') }}</div>
@@ -165,4 +174,4 @@
{{ t('header.filter.clearAll') }}
</button>
</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})
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