Add recipes checkpoint

This commit is contained in:
Will Miao
2025-03-08 23:10:24 +08:00
parent e8e5012f0c
commit e6aafe8773
12 changed files with 1581 additions and 5 deletions

365
templates/recipes.html Normal file
View File

@@ -0,0 +1,365 @@
<!DOCTYPE html>
<html>
<head>
<title>LoRA Recipes</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/loras_static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
<link rel="manifest" href="/loras_static/images/site.webmanifest">
<!-- Preload critical resources -->
<link rel="preload" href="/loras_static/css/style.css" as="style">
<link rel="preload" href="/loras_static/js/recipes.js" as="script" crossorigin="anonymous">
<!-- Optimize font loading -->
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
<!-- Performance monitoring -->
<script>
performance.mark('page-start');
window.addEventListener('load', () => {
performance.mark('page-end');
performance.measure('page-load', 'page-start', 'page-end');
});
</script>
<!-- Security meta tags -->
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
<!-- Resource loading strategy -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
<style>
/* Recipe-specific styles */
.recipe-tag-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.recipe-tag {
background: var(--lora-surface-hover);
color: var(--lora-text-secondary);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius-sm);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
}
.recipe-tag:hover, .recipe-tag.active {
background: var(--lora-primary);
color: var(--lora-text-on-primary);
}
.recipe-card {
position: relative;
background: var(--lora-surface);
border-radius: var(--border-radius-base);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
cursor: pointer;
display: flex;
flex-direction: column;
}
.recipe-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.recipe-indicator {
position: absolute;
top: 8px;
left: 8px;
width: 24px;
height: 24px;
background: var(--lora-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
z-index: 2;
}
.recipe-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.placeholder-message {
grid-column: 1 / -1;
text-align: center;
padding: 2rem;
background: var(--lora-surface-alt);
border-radius: var(--border-radius-base);
}
.recipes-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.recipes-controls {
display: flex;
gap: 0.5rem;
}
</style>
</head>
<body>
<div class="corner-controls">
<div class="corner-controls-toggle">
<i class="fas fa-ellipsis-v"></i>
<span class="update-badge corner-badge hidden"></span>
</div>
<div class="corner-controls-items">
<div class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">
<img src="/loras_static/images/theme-toggle-light.svg" alt="Theme" class="theme-icon light-icon">
<img src="/loras_static/images/theme-toggle-dark.svg" alt="Theme" class="theme-icon dark-icon">
</div>
<div class="update-toggle" id="updateToggleBtn" title="Check Updates">
<i class="fas fa-bell"></i>
<span class="update-badge hidden"></span>
</div>
<div class="support-toggle" id="supportToggleBtn" title="Support">
<i class="fas fa-heart"></i>
</div>
<div class="settings-toggle" onclick="settingsManager.toggleSettings()" title="Settings">
<i class="fas fa-cog"></i>
</div>
</div>
</div>
{% include 'components/modals.html' %}
{% include 'components/loading.html' %}
{% include 'components/context_menu.html' %}
<div class="container">
{% if is_initializing %}
<div class="initialization-notice">
<div class="notice-content">
<div class="loading-spinner"></div>
<h2>Initializing Recipe Manager</h2>
<p>Scanning and building recipe cache. This may take a few moments...</p>
</div>
</div>
{% else %}
<div class="recipes-header">
<h1>LoRA Recipes</h1>
<div class="recipes-controls">
<a href="/loras" class="button"><i class="fas fa-arrow-left"></i> Back to LoRAs</a>
</div>
</div>
<!-- Recipe controls -->
<div class="controls">
<div class="folder-tags-container">
<div class="recipe-tag-container">
{% if recipe_tags %}
{% for tag in recipe_tags %}
<div class="recipe-tag" data-tag="{{ tag }}">{{ tag }}</div>
{% endfor %}
{% else %}
<div class="recipe-tag" data-tag="all">All</div>
{% endif %}
</div>
<button class="toggle-folders-btn" onclick="toggleTagsContainer()" title="Collapse tags">
<i class="fas fa-chevron-up"></i>
</button>
</div>
<div class="actions">
<div title="Sort recipes by..." class="control-group">
<select id="sortSelect">
<option value="date">Date</option>
<option value="name">Name</option>
</select>
</div>
<div title="Refresh recipes list" class="control-group">
<button onclick="refreshRecipes()"><i class="fas fa-sync"></i> Refresh</button>
</div>
<div class="search-container">
<input type="text" id="searchInput" placeholder="Search recipes..." />
<i class="fas fa-search search-icon"></i>
<button class="search-filter-toggle" id="filterButton" onclick="recipeFilterManager.toggleFilterPanel()" title="Filter recipes">
<i class="fas fa-filter"></i>
<span class="filter-badge" id="activeFiltersCount" style="display: none">0</span>
</button>
</div>
</div>
</div>
<!-- Recipe filter panel -->
<div id="filterPanel" class="filter-panel hidden">
<div class="filter-header">
<h3>Filter Recipes</h3>
<button class="close-filter-btn" onclick="recipeFilterManager.closeFilterPanel()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="filter-section">
<h4>Base Model</h4>
<div class="filter-tags" id="baseModelTags">
<!-- Tags will be dynamically inserted here -->
</div>
</div>
<div class="filter-section">
<h4>LoRAs</h4>
<div class="filter-tags" id="loraModelTags">
<!-- LoRA tags will be dynamically inserted here -->
</div>
</div>
<div class="filter-actions">
<button class="clear-filters-btn" onclick="recipeFilterManager.clearFilters()">
Clear All Filters
</button>
</div>
</div>
<!-- Recipe gallery container -->
<div class="recipe-grid" id="recipeGrid">
{% if recipes and recipes|length > 0 %}
{% for recipe in recipes %}
<div class="recipe-card" data-file-path="{{ recipe.file_path }}" data-title="{{ recipe.title }}" data-created="{{ recipe.created_date }}">
<div class="recipe-indicator" title="Recipe">R</div>
<div class="card-preview">
<img src="{{ recipe.file_url }}" alt="{{ recipe.title }}">
<div class="card-header">
{% if recipe.base_model %}
<span class="base-model-label" title="{{ recipe.base_model }}">
{{ recipe.base_model }}
</span>
{% endif %}
</div>
<div class="card-footer">
<div class="model-info">
<span class="model-name">{{ recipe.title }}</span>
</div>
<div class="lora-count" title="Number of LoRAs in this recipe">
<i class="fas fa-layer-group"></i> {{ recipe.loras|length }}
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="placeholder-message">
<p>No recipes found</p>
<p>Add recipe images to your recipes folder to see them here.</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<script type="module" src="/loras_static/js/recipes.js"></script>
{% if is_initializing %}
<script>
// Check initialization status and set auto-refresh
async function checkInitStatus() {
try {
const response = await fetch('/api/recipes?page=1&page_size=1');
if (response.ok) {
// If data successfully retrieved, initialization is complete, refresh the page
window.location.reload();
} else {
// If not yet complete, continue polling
setTimeout(checkInitStatus, 2000); // Check every 2 seconds
}
} catch (error) {
// If error, continue polling
setTimeout(checkInitStatus, 2000);
}
}
// Start status checking
checkInitStatus();
</script>
{% endif %}
<!-- Recipe page specific scripts -->
<script>
// Toggle recipe tags container
function toggleTagsContainer() {
const container = document.querySelector('.recipe-tag-container');
const button = document.querySelector('.toggle-folders-btn');
if (container.style.display === 'none') {
container.style.display = 'flex';
button.querySelector('i').className = 'fas fa-chevron-up';
button.title = 'Collapse tags';
} else {
container.style.display = 'none';
button.querySelector('i').className = 'fas fa-chevron-down';
button.title = 'Expand tags';
}
}
// Refresh recipes
function refreshRecipes() {
// Will be implemented in recipes.js
window.location.reload();
}
// Simple utility function to handle theme toggling
function toggleTheme() {
document.body.classList.toggle('dark-theme');
localStorage.setItem('theme', document.body.classList.contains('dark-theme') ? 'dark' : 'light');
}
// Apply saved theme on load
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
}
// Setup recipe tag filtering
document.querySelectorAll('.recipe-tag').forEach(tag => {
tag.addEventListener('click', () => {
document.querySelectorAll('.recipe-tag').forEach(t => t.classList.remove('active'));
tag.classList.add('active');
// Implement filtering logic here or in recipes.js
console.log('Filter by tag:', tag.dataset.tag);
});
});
});
// Placeholder for recipe filter manager
const recipeFilterManager = {
toggleFilterPanel() {
const panel = document.getElementById('filterPanel');
panel.classList.toggle('hidden');
},
closeFilterPanel() {
document.getElementById('filterPanel').classList.add('hidden');
},
clearFilters() {
// Clear filters and reset UI
document.querySelectorAll('.filter-tags .tag.active').forEach(tag => {
tag.classList.remove('active');
});
document.getElementById('activeFiltersCount').style.display = 'none';
// Reapply default view
refreshRecipes();
}
};
</script>
</body>
</html>