mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
fix(recipe): hydrate stale modal data from recipe json
This commit is contained in:
@@ -2114,8 +2114,18 @@ class RecipeScanner:
|
|||||||
if not recipe:
|
if not recipe:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Prefer the on-disk recipe JSON for fields that are not persisted in the
|
||||||
|
# SQLite cache yet, such as source_path.
|
||||||
|
merged_recipe = {**recipe}
|
||||||
|
recipe_json = await self._load_recipe_json(recipe_id)
|
||||||
|
if recipe_json:
|
||||||
|
for field in ("source_path", "checkpoint", "loras", "gen_params"):
|
||||||
|
if field not in recipe_json:
|
||||||
|
merged_recipe.pop(field, None)
|
||||||
|
merged_recipe.update(recipe_json)
|
||||||
|
|
||||||
# Format the recipe with all needed information
|
# Format the recipe with all needed information
|
||||||
formatted_recipe = {**recipe} # Copy all fields
|
formatted_recipe = {**merged_recipe}
|
||||||
|
|
||||||
# Format file path to URL
|
# Format file path to URL
|
||||||
if "file_path" in formatted_recipe:
|
if "file_path" in formatted_recipe:
|
||||||
@@ -2149,6 +2159,30 @@ class RecipeScanner:
|
|||||||
|
|
||||||
return formatted_recipe
|
return formatted_recipe
|
||||||
|
|
||||||
|
async def _load_recipe_json(self, recipe_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Load the raw recipe JSON payload for a recipe ID if it exists."""
|
||||||
|
|
||||||
|
recipe_json_path = await self.get_recipe_json_path(recipe_id)
|
||||||
|
if not recipe_json_path or not os.path.exists(recipe_json_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(recipe_json_path, "r", encoding="utf-8") as f:
|
||||||
|
recipe_data = json.load(f)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(
|
||||||
|
"Failed to load recipe JSON for %s from %s: %s",
|
||||||
|
recipe_id,
|
||||||
|
recipe_json_path,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(recipe_data, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return recipe_data
|
||||||
|
|
||||||
def _format_file_url(self, file_path: str) -> str:
|
def _format_file_url(self, file_path: str) -> str:
|
||||||
"""Format file path as URL for serving in web UI"""
|
"""Format file path as URL for serving in web UI"""
|
||||||
if not file_path:
|
if not file_path:
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ export function extractRecipeId(filePath) {
|
|||||||
return dotIndex > 0 ? basename.substring(0, dotIndex) : basename;
|
return dotIndex > 0 ? basename.substring(0, dotIndex) : basename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchRecipeDetails(recipeId) {
|
||||||
|
if (!recipeId) {
|
||||||
|
throw new Error('Unable to determine recipe ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedRecipeId = encodeURIComponent(recipeId);
|
||||||
|
const response = await fetch(`${RECIPE_ENDPOINTS.detail}/${encodedRecipeId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch recipes with pagination for virtual scrolling
|
* Fetch recipes with pagination for virtual scrolling
|
||||||
* @param {number} page - Page number to fetch
|
* @param {number} page - Page number to fetch
|
||||||
@@ -61,7 +75,9 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
|||||||
// If we have a specific recipe ID to load
|
// If we have a specific recipe ID to load
|
||||||
if (pageState.customFilter?.active && pageState.customFilter?.recipeId) {
|
if (pageState.customFilter?.active && pageState.customFilter?.recipeId) {
|
||||||
// Special case: load specific recipe
|
// Special case: load specific recipe
|
||||||
const response = await fetch(`${RECIPE_ENDPOINTS.detail}/${pageState.customFilter.recipeId}`);
|
const response = await fetch(
|
||||||
|
`${RECIPE_ENDPOINTS.detail}/${encodeURIComponent(pageState.customFilter.recipeId)}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
||||||
@@ -349,9 +365,10 @@ export function createRecipeCard(recipe) {
|
|||||||
* @param {Object} updates - The metadata updates to apply
|
* @param {Object} updates - The metadata updates to apply
|
||||||
* @returns {Promise<Object>} The updated recipe data
|
* @returns {Promise<Object>} The updated recipe data
|
||||||
*/
|
*/
|
||||||
export async function updateRecipeMetadata(filePath, updates) {
|
export async function updateRecipeMetadata(filePath, updates, options = {}) {
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading('Saving metadata...');
|
state.loadingManager.showSimpleLoading('Saving metadata...');
|
||||||
|
const listFilePath = options.listFilePath || filePath;
|
||||||
|
|
||||||
// Extract recipeId from filePath (basename without extension)
|
// Extract recipeId from filePath (basename without extension)
|
||||||
const recipeId = extractRecipeId(filePath);
|
const recipeId = extractRecipeId(filePath);
|
||||||
@@ -359,7 +376,7 @@ export async function updateRecipeMetadata(filePath, updates) {
|
|||||||
throw new Error('Unable to determine recipe ID');
|
throw new Error('Unable to determine recipe ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${RECIPE_ENDPOINTS.update}/${recipeId}/update`, {
|
const response = await fetch(`${RECIPE_ENDPOINTS.update}/${encodeURIComponent(recipeId)}/update`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -374,7 +391,7 @@ export async function updateRecipeMetadata(filePath, updates) {
|
|||||||
throw new Error(data.error || 'Failed to update recipe');
|
throw new Error(data.error || 'Failed to update recipe');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.virtualScroller.updateSingleItem(filePath, updates);
|
state.virtualScroller.updateSingleItem(listFilePath, updates);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,16 +3,75 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow
|
|||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
||||||
import { updateRecipeMetadata } from '../api/recipeApi.js';
|
import { fetchRecipeDetails, updateRecipeMetadata } from '../api/recipeApi.js';
|
||||||
import { downloadManager } from '../managers/DownloadManager.js';
|
import { downloadManager } from '../managers/DownloadManager.js';
|
||||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||||
|
|
||||||
class RecipeModal {
|
class RecipeModal {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.promptEditorState = {};
|
this.promptEditorState = {};
|
||||||
|
this.recipeHydrationRequestId = 0;
|
||||||
|
this.resetLocalEditState();
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLocalEditState() {
|
||||||
|
return {
|
||||||
|
title: { commitVersion: 0, isDirty: false },
|
||||||
|
tags: { commitVersion: 0, isDirty: false },
|
||||||
|
prompt: { commitVersion: 0, isDirty: false },
|
||||||
|
negative_prompt: { commitVersion: 0, isDirty: false },
|
||||||
|
source_path: { commitVersion: 0, isDirty: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLocalEditState() {
|
||||||
|
this.localEditState = this.createLocalEditState();
|
||||||
|
this.sourceUrlEditState = this.localEditState.source_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocalEditState(field) {
|
||||||
|
if (!this.localEditState[field]) {
|
||||||
|
this.localEditState[field] = { commitVersion: 0, isDirty: false };
|
||||||
|
}
|
||||||
|
return this.localEditState[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
markFieldDirty(field) {
|
||||||
|
this.getLocalEditState(field).isDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFieldDirty(field) {
|
||||||
|
this.getLocalEditState(field).isDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitField(field) {
|
||||||
|
const fieldState = this.getLocalEditState(field);
|
||||||
|
fieldState.isDirty = false;
|
||||||
|
fieldState.commitVersion += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
captureLocalEditVersions() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.localEditState).map(([field, state]) => [
|
||||||
|
field,
|
||||||
|
state.commitVersion,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldPreserveField(field, requestVersions) {
|
||||||
|
const fieldState = this.getLocalEditState(field);
|
||||||
|
const requestVersion = requestVersions?.[field] ?? fieldState.commitVersion;
|
||||||
|
return fieldState.isDirty || fieldState.commitVersion !== requestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFieldCommittedSinceRequest(field, requestVersions) {
|
||||||
|
const fieldState = this.getLocalEditState(field);
|
||||||
|
const requestVersion = requestVersions?.[field] ?? fieldState.commitVersion;
|
||||||
|
return fieldState.commitVersion !== requestVersion;
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.setupCopyButtons();
|
this.setupCopyButtons();
|
||||||
this.setupPromptEditors();
|
this.setupPromptEditors();
|
||||||
@@ -87,8 +146,10 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showRecipeDetails(recipe) {
|
showRecipeDetails(recipe) {
|
||||||
|
const hydratedRecipe = recipe || {};
|
||||||
|
this.resetLocalEditState();
|
||||||
// Store the full recipe for editing
|
// Store the full recipe for editing
|
||||||
this.currentRecipe = recipe;
|
this.currentRecipe = hydratedRecipe;
|
||||||
this.resetPromptEditors();
|
this.resetPromptEditors();
|
||||||
|
|
||||||
// Set modal title with edit icon
|
// Set modal title with edit icon
|
||||||
@@ -96,11 +157,11 @@ class RecipeModal {
|
|||||||
if (modalTitle) {
|
if (modalTitle) {
|
||||||
modalTitle.innerHTML = `
|
modalTitle.innerHTML = `
|
||||||
<div class="editable-content">
|
<div class="editable-content">
|
||||||
<span class="content-text">${recipe.title || 'Recipe Details'}</span>
|
<span class="content-text">${hydratedRecipe.title || 'Recipe Details'}</span>
|
||||||
<button class="edit-icon" title="Edit recipe name"><i class="fas fa-pencil-alt"></i></button>
|
<button class="edit-icon" title="Edit recipe name"><i class="fas fa-pencil-alt"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="recipeTitleEditor" class="content-editor">
|
<div id="recipeTitleEditor" class="content-editor">
|
||||||
<input type="text" class="title-input" value="${recipe.title || ''}">
|
<input type="text" class="title-input" value="${hydratedRecipe.title || ''}">
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -122,8 +183,9 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the recipe ID for copy syntax API call
|
// Store the recipe ID for copy syntax API call
|
||||||
this.recipeId = recipe.id;
|
this.recipeId = hydratedRecipe.id;
|
||||||
this.filePath = recipe.file_path;
|
this.filePath = hydratedRecipe.file_path;
|
||||||
|
this.listFilePath = hydratedRecipe.file_path;
|
||||||
|
|
||||||
// Set recipe tags if they exist
|
// Set recipe tags if they exist
|
||||||
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
||||||
@@ -143,11 +205,11 @@ class RecipeModal {
|
|||||||
|
|
||||||
const tagsDisplay = tagsCompactElement.querySelector('.tags-display');
|
const tagsDisplay = tagsCompactElement.querySelector('.tags-display');
|
||||||
|
|
||||||
if (recipe.tags && recipe.tags.length > 0) {
|
if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) {
|
||||||
// Limit displayed tags to 5, show a "+X more" button if needed
|
// Limit displayed tags to 5, show a "+X more" button if needed
|
||||||
const maxVisibleTags = 5;
|
const maxVisibleTags = 5;
|
||||||
const visibleTags = recipe.tags.slice(0, maxVisibleTags);
|
const visibleTags = hydratedRecipe.tags.slice(0, maxVisibleTags);
|
||||||
const remainingTags = recipe.tags.length > maxVisibleTags ? recipe.tags.slice(maxVisibleTags) : [];
|
const remainingTags = hydratedRecipe.tags.length > maxVisibleTags ? hydratedRecipe.tags.slice(maxVisibleTags) : [];
|
||||||
|
|
||||||
// Add visible tags
|
// Add visible tags
|
||||||
visibleTags.forEach(tag => {
|
visibleTags.forEach(tag => {
|
||||||
@@ -184,7 +246,7 @@ class RecipeModal {
|
|||||||
// Add all tags to tooltip
|
// Add all tags to tooltip
|
||||||
if (tagsTooltipContent) {
|
if (tagsTooltipContent) {
|
||||||
tagsTooltipContent.innerHTML = '';
|
tagsTooltipContent.innerHTML = '';
|
||||||
recipe.tags.forEach(tag => {
|
hydratedRecipe.tags.forEach(tag => {
|
||||||
const tooltipTag = document.createElement('div');
|
const tooltipTag = document.createElement('div');
|
||||||
tooltipTag.className = 'tooltip-tag';
|
tooltipTag.className = 'tooltip-tag';
|
||||||
tooltipTag.textContent = tag;
|
tooltipTag.textContent = tag;
|
||||||
@@ -201,8 +263,8 @@ class RecipeModal {
|
|||||||
const tagsInput = tagsCompactElement.querySelector('.tags-input');
|
const tagsInput = tagsCompactElement.querySelector('.tags-input');
|
||||||
|
|
||||||
// Set current tags in the input
|
// Set current tags in the input
|
||||||
if (recipe.tags && recipe.tags.length > 0) {
|
if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) {
|
||||||
tagsInput.value = recipe.tags.join(', ');
|
tagsInput.value = hydratedRecipe.tags.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
editTagsIcon.addEventListener('click', () => this.showTagsEditor());
|
editTagsIcon.addEventListener('click', () => this.showTagsEditor());
|
||||||
@@ -222,49 +284,15 @@ class RecipeModal {
|
|||||||
// Set recipe image
|
// Set recipe image
|
||||||
const mediaContainer = document.getElementById('recipePreviewContainer');
|
const mediaContainer = document.getElementById('recipePreviewContainer');
|
||||||
if (mediaContainer) {
|
if (mediaContainer) {
|
||||||
// Stop any playing video before replacing content
|
this.syncPreviewMedia(hydratedRecipe);
|
||||||
const existingVideo = mediaContainer.querySelector('video');
|
mediaContainer.querySelector('.source-url-container')?.remove();
|
||||||
if (existingVideo) {
|
mediaContainer.querySelector('.source-url-editor')?.remove();
|
||||||
existingVideo.pause();
|
|
||||||
existingVideo.currentTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the container
|
|
||||||
mediaContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// Ensure file_url exists, fallback to file_path if needed
|
|
||||||
const imageUrl = recipe.file_url ||
|
|
||||||
(recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` :
|
|
||||||
'/loras_static/images/no-preview.png');
|
|
||||||
|
|
||||||
// Check if the file is a video (mp4)
|
|
||||||
const isVideo = imageUrl.toLowerCase().endsWith('.mp4');
|
|
||||||
|
|
||||||
if (isVideo) {
|
|
||||||
const videoElement = document.createElement('video');
|
|
||||||
videoElement.id = 'recipeModalVideo';
|
|
||||||
videoElement.src = imageUrl;
|
|
||||||
videoElement.controls = true;
|
|
||||||
videoElement.autoplay = false;
|
|
||||||
videoElement.loop = true;
|
|
||||||
videoElement.muted = true;
|
|
||||||
videoElement.className = 'recipe-preview-media';
|
|
||||||
videoElement.alt = recipe.title || 'Recipe Preview';
|
|
||||||
mediaContainer.appendChild(videoElement);
|
|
||||||
} else {
|
|
||||||
const imgElement = document.createElement('img');
|
|
||||||
imgElement.id = 'recipeModalImage';
|
|
||||||
imgElement.src = imageUrl;
|
|
||||||
imgElement.className = 'recipe-preview-media';
|
|
||||||
imgElement.alt = recipe.title || 'Recipe Preview';
|
|
||||||
mediaContainer.appendChild(imgElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add source URL container if the recipe has a source_path
|
// Add source URL container if the recipe has a source_path
|
||||||
const sourceUrlContainer = document.createElement('div');
|
const sourceUrlContainer = document.createElement('div');
|
||||||
sourceUrlContainer.className = 'source-url-container';
|
sourceUrlContainer.className = 'source-url-container';
|
||||||
const hasSourceUrl = recipe.source_path && recipe.source_path.trim().length > 0;
|
const hasSourceUrl = hydratedRecipe.source_path && hydratedRecipe.source_path.trim().length > 0;
|
||||||
const sourceUrl = hasSourceUrl ? recipe.source_path : '';
|
const sourceUrl = hasSourceUrl ? hydratedRecipe.source_path : '';
|
||||||
const isValidUrl = hasSourceUrl && (sourceUrl.startsWith('http://') || sourceUrl.startsWith('https://'));
|
const isValidUrl = hasSourceUrl && (sourceUrl.startsWith('http://') || sourceUrl.startsWith('https://'));
|
||||||
|
|
||||||
sourceUrlContainer.innerHTML = `
|
sourceUrlContainer.innerHTML = `
|
||||||
@@ -299,34 +327,261 @@ class RecipeModal {
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set generation parameters
|
this.syncGenerationParams(hydratedRecipe.gen_params);
|
||||||
|
this.syncResourcesSection(hydratedRecipe);
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
modalManager.showModal('recipeModal');
|
||||||
|
|
||||||
|
if (this.recipeId) {
|
||||||
|
const hydrationRequestId = ++this.recipeHydrationRequestId;
|
||||||
|
const requestEditVersions = this.captureLocalEditVersions();
|
||||||
|
this.hydrateRecipeDetails(
|
||||||
|
this.recipeId,
|
||||||
|
hydrationRequestId,
|
||||||
|
requestEditVersions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async hydrateRecipeDetails(recipeId, requestId, requestEditVersions = {}) {
|
||||||
|
try {
|
||||||
|
const fullRecipe = await fetchRecipeDetails(recipeId);
|
||||||
|
if (requestId !== this.recipeHydrationRequestId || !fullRecipe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRecipe = { ...this.currentRecipe };
|
||||||
|
|
||||||
|
if (!this.hasFieldCommittedSinceRequest('title', requestEditVersions) && fullRecipe.title !== undefined) {
|
||||||
|
nextRecipe.title = fullRecipe.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasFieldCommittedSinceRequest('tags', requestEditVersions) && fullRecipe.tags !== undefined) {
|
||||||
|
nextRecipe.tags = Array.isArray(fullRecipe.tags) ? [...fullRecipe.tags] : fullRecipe.tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hasFieldCommittedSinceRequest('source_path', requestEditVersions)) {
|
||||||
|
nextRecipe.source_path = fullRecipe.source_path || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousFilePath = nextRecipe.file_path;
|
||||||
|
if (fullRecipe.file_path !== undefined) {
|
||||||
|
nextRecipe.file_path = fullRecipe.file_path;
|
||||||
|
}
|
||||||
|
if (fullRecipe.file_url !== undefined) {
|
||||||
|
nextRecipe.file_url = fullRecipe.file_url;
|
||||||
|
}
|
||||||
|
if (fullRecipe.preview_url !== undefined) {
|
||||||
|
nextRecipe.preview_url = fullRecipe.preview_url;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fullRecipe.file_path !== undefined &&
|
||||||
|
fullRecipe.file_path !== previousFilePath &&
|
||||||
|
fullRecipe.file_url === undefined &&
|
||||||
|
fullRecipe.preview_url === undefined
|
||||||
|
) {
|
||||||
|
delete nextRecipe.file_url;
|
||||||
|
delete nextRecipe.preview_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullRecipe.gen_params !== undefined) {
|
||||||
|
const previousGenParams = nextRecipe.gen_params || {};
|
||||||
|
const incomingGenParams = { ...(fullRecipe.gen_params || {}) };
|
||||||
|
for (const [key, value] of Object.entries(previousGenParams)) {
|
||||||
|
if (this.hasFieldCommittedSinceRequest(key, requestEditVersions)) {
|
||||||
|
incomingGenParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextRecipe.gen_params = incomingGenParams;
|
||||||
|
} else {
|
||||||
|
const previousGenParams = nextRecipe.gen_params || {};
|
||||||
|
const preservedGenParams = {};
|
||||||
|
for (const [key, value] of Object.entries(previousGenParams)) {
|
||||||
|
if (this.hasFieldCommittedSinceRequest(key, requestEditVersions)) {
|
||||||
|
preservedGenParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextRecipe.gen_params = preservedGenParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullRecipe.checkpoint !== undefined) {
|
||||||
|
nextRecipe.checkpoint = fullRecipe.checkpoint;
|
||||||
|
} else {
|
||||||
|
delete nextRecipe.checkpoint;
|
||||||
|
}
|
||||||
|
if (fullRecipe.loras !== undefined) {
|
||||||
|
nextRecipe.loras = Array.isArray(fullRecipe.loras) ? [...fullRecipe.loras] : fullRecipe.loras;
|
||||||
|
} else {
|
||||||
|
delete nextRecipe.loras;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentRecipe = nextRecipe;
|
||||||
|
this.filePath = this.currentRecipe.file_path || this.filePath;
|
||||||
|
|
||||||
|
this.syncHydratedRecipeFields(requestEditVersions);
|
||||||
|
} catch (error) {
|
||||||
|
// Keep the cached recipe visible if hydration fails.
|
||||||
|
console.warn('Failed to hydrate recipe details:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncHydratedRecipeFields(requestEditVersions = {}) {
|
||||||
|
this.syncPreviewMedia(this.currentRecipe);
|
||||||
|
|
||||||
|
if (!this.shouldPreserveField('title', requestEditVersions)) {
|
||||||
|
this.syncTitleDisplay(this.currentRecipe?.title || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.shouldPreserveField('tags', requestEditVersions)) {
|
||||||
|
this.syncTagsDisplay(this.currentRecipe?.tags || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.shouldPreserveField('prompt', requestEditVersions)) {
|
||||||
|
this.syncPromptField(
|
||||||
|
'prompt',
|
||||||
|
this.currentRecipe?.gen_params?.prompt || '',
|
||||||
|
'No prompt information available'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.shouldPreserveField('negative_prompt', requestEditVersions)) {
|
||||||
|
this.syncPromptField(
|
||||||
|
'negative_prompt',
|
||||||
|
this.currentRecipe?.gen_params?.negative_prompt || '',
|
||||||
|
'No negative prompt information available'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncGenerationParams(this.currentRecipe?.gen_params, { promptFieldsOnly: true });
|
||||||
|
this.syncResourcesSection(this.currentRecipe);
|
||||||
|
|
||||||
|
if (!this.shouldPreserveField('source_path', requestEditVersions)) {
|
||||||
|
this.updateSourceUrlDisplay(this.currentRecipe.source_path || '', { forceInputSync: true });
|
||||||
|
} else {
|
||||||
|
this.updateSourceUrlDisplay(this.currentRecipe.source_path || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviewMediaUrl(recipe = {}) {
|
||||||
|
return recipe.file_url ||
|
||||||
|
recipe.preview_url ||
|
||||||
|
(recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` :
|
||||||
|
'/loras_static/images/no-preview.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPreviewMedia(recipe = {}) {
|
||||||
|
const mediaContainer = document.getElementById('recipePreviewContainer');
|
||||||
|
if (!mediaContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewUrl = this.getPreviewMediaUrl(recipe);
|
||||||
|
const isVideo = previewUrl.toLowerCase().endsWith('.mp4');
|
||||||
|
const expectedElementId = isVideo ? 'recipeModalVideo' : 'recipeModalImage';
|
||||||
|
let previewElement = mediaContainer.querySelector(`#${expectedElementId}`);
|
||||||
|
const existingPreviewElement = mediaContainer.querySelector('.recipe-preview-media');
|
||||||
|
|
||||||
|
if (!previewElement || (existingPreviewElement && existingPreviewElement !== previewElement)) {
|
||||||
|
if (existingPreviewElement?.tagName === 'VIDEO') {
|
||||||
|
const existingVideo = existingPreviewElement;
|
||||||
|
existingVideo.pause();
|
||||||
|
existingVideo.currentTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingPreviewElement?.remove();
|
||||||
|
previewElement = document.createElement(isVideo ? 'video' : 'img');
|
||||||
|
previewElement.id = expectedElementId;
|
||||||
|
previewElement.className = 'recipe-preview-media';
|
||||||
|
mediaContainer.prepend(previewElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
previewElement.src = previewUrl;
|
||||||
|
previewElement.alt = recipe.title || 'Recipe Preview';
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
previewElement.controls = true;
|
||||||
|
previewElement.autoplay = false;
|
||||||
|
previewElement.loop = true;
|
||||||
|
previewElement.muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadataUpdateOptions() {
|
||||||
|
return this.listFilePath ? { listFilePath: this.listFilePath } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTitleDisplay(title) {
|
||||||
|
const titleContainer = document.getElementById('recipeModalTitle');
|
||||||
|
if (!titleContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentText = titleContainer.querySelector('.content-text');
|
||||||
|
if (contentText) {
|
||||||
|
contentText.textContent = title || 'Recipe Details';
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleInput = titleContainer.querySelector('.title-input');
|
||||||
|
if (titleInput) {
|
||||||
|
titleInput.value = title || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTagsDisplay(tags) {
|
||||||
|
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||||
|
if (!tagsContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTagsDisplay(tagsContainer, tags || []);
|
||||||
|
|
||||||
|
const tagsInput = tagsContainer.querySelector('.tags-input');
|
||||||
|
if (tagsInput) {
|
||||||
|
tagsInput.value = tags && tags.length > 0 ? tags.join(', ') : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPromptField(field, value, placeholder) {
|
||||||
|
const contentId = field === 'prompt' ? 'recipePrompt' : 'recipeNegativePrompt';
|
||||||
|
const editorId = field === 'prompt' ? 'recipePromptEditor' : 'recipeNegativePromptEditor';
|
||||||
|
const inputId = field === 'prompt' ? 'recipePromptInput' : 'recipeNegativePromptInput';
|
||||||
|
|
||||||
|
this.renderPromptContent(document.getElementById(contentId), value, placeholder);
|
||||||
|
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (input) {
|
||||||
|
input.value = value || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGenerationParams(genParams, options = {}) {
|
||||||
const promptElement = document.getElementById('recipePrompt');
|
const promptElement = document.getElementById('recipePrompt');
|
||||||
const negativePromptElement = document.getElementById('recipeNegativePrompt');
|
const negativePromptElement = document.getElementById('recipeNegativePrompt');
|
||||||
const otherParamsElement = document.getElementById('recipeOtherParams');
|
const otherParamsElement = document.getElementById('recipeOtherParams');
|
||||||
const promptInput = document.getElementById('recipePromptInput');
|
const promptInput = document.getElementById('recipePromptInput');
|
||||||
const negativePromptInput = document.getElementById('recipeNegativePromptInput');
|
const negativePromptInput = document.getElementById('recipeNegativePromptInput');
|
||||||
|
const promptFieldsOnly = options.promptFieldsOnly === true;
|
||||||
|
|
||||||
if (recipe.gen_params) {
|
if (genParams) {
|
||||||
this.renderPromptContent(promptElement, recipe.gen_params.prompt, 'No prompt information available');
|
if (!promptFieldsOnly) {
|
||||||
this.renderPromptContent(negativePromptElement, recipe.gen_params.negative_prompt, 'No negative prompt information available');
|
this.renderPromptContent(promptElement, genParams.prompt, 'No prompt information available');
|
||||||
|
this.renderPromptContent(negativePromptElement, genParams.negative_prompt, 'No negative prompt information available');
|
||||||
|
|
||||||
if (promptInput) {
|
if (promptInput) {
|
||||||
promptInput.value = recipe.gen_params.prompt || '';
|
promptInput.value = genParams.prompt || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (negativePromptInput) {
|
||||||
|
negativePromptInput.value = genParams.negative_prompt || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (negativePromptInput) {
|
|
||||||
negativePromptInput.value = recipe.gen_params.negative_prompt || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set other parameters
|
|
||||||
if (otherParamsElement) {
|
if (otherParamsElement) {
|
||||||
// Clear previous params
|
|
||||||
otherParamsElement.innerHTML = '';
|
otherParamsElement.innerHTML = '';
|
||||||
|
|
||||||
// Add all other parameters except prompt and negative_prompt
|
|
||||||
const excludedParams = ['prompt', 'negative_prompt'];
|
const excludedParams = ['prompt', 'negative_prompt'];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(recipe.gen_params)) {
|
for (const [key, value] of Object.entries(genParams)) {
|
||||||
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
||||||
const paramTag = document.createElement('div');
|
const paramTag = document.createElement('div');
|
||||||
paramTag.className = 'param-tag';
|
paramTag.className = 'param-tag';
|
||||||
@@ -338,22 +593,31 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no other params, show a message
|
|
||||||
if (otherParamsElement.children.length === 0) {
|
if (otherParamsElement.children.length === 0) {
|
||||||
otherParamsElement.innerHTML = '<div class="no-params">No additional parameters available</div>';
|
otherParamsElement.innerHTML = '<div class="no-params">No additional parameters available</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return;
|
||||||
// No generation parameters available
|
}
|
||||||
|
|
||||||
|
if (!promptFieldsOnly) {
|
||||||
this.renderPromptContent(promptElement, '', 'No prompt information available');
|
this.renderPromptContent(promptElement, '', 'No prompt information available');
|
||||||
this.renderPromptContent(negativePromptElement, '', 'No negative prompt information available');
|
this.renderPromptContent(negativePromptElement, '', 'No negative prompt information available');
|
||||||
if (promptInput) promptInput.value = '';
|
if (promptInput) promptInput.value = '';
|
||||||
if (negativePromptInput) negativePromptInput.value = '';
|
if (negativePromptInput) negativePromptInput.value = '';
|
||||||
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (otherParamsElement) {
|
||||||
|
otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncResourcesSection(recipe = {}) {
|
||||||
const checkpointContainer = document.getElementById('recipeCheckpoint');
|
const checkpointContainer = document.getElementById('recipeCheckpoint');
|
||||||
const resourceDivider = document.getElementById('recipeResourceDivider');
|
const resourceDivider = document.getElementById('recipeResourceDivider');
|
||||||
|
const lorasListElement = document.getElementById('recipeLorasList');
|
||||||
|
const lorasCountElement = document.getElementById('recipeLorasCount');
|
||||||
|
const loras = Array.isArray(recipe.loras) ? recipe.loras : [];
|
||||||
|
|
||||||
if (checkpointContainer) {
|
if (checkpointContainer) {
|
||||||
checkpointContainer.innerHTML = '';
|
checkpointContainer.innerHTML = '';
|
||||||
@@ -364,59 +628,43 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set LoRAs list and count
|
|
||||||
const lorasListElement = document.getElementById('recipeLorasList');
|
|
||||||
const lorasCountElement = document.getElementById('recipeLorasCount');
|
|
||||||
|
|
||||||
// Check all LoRAs status
|
|
||||||
let allLorasAvailable = true;
|
let allLorasAvailable = true;
|
||||||
let missingLorasCount = 0;
|
let missingLorasCount = 0;
|
||||||
let deletedLorasCount = 0;
|
let deletedLorasCount = 0;
|
||||||
|
|
||||||
if (recipe.loras && recipe.loras.length > 0) {
|
loras.forEach(lora => {
|
||||||
recipe.loras.forEach(lora => {
|
if (lora.isDeleted) {
|
||||||
if (lora.isDeleted) {
|
deletedLorasCount++;
|
||||||
deletedLorasCount++;
|
} else if (!lora.inLibrary) {
|
||||||
} else if (!lora.inLibrary) {
|
allLorasAvailable = false;
|
||||||
allLorasAvailable = false;
|
missingLorasCount++;
|
||||||
missingLorasCount++;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set LoRAs count and status
|
if (lorasCountElement) {
|
||||||
if (lorasCountElement && recipe.loras) {
|
const totalCount = loras.length;
|
||||||
const totalCount = recipe.loras.length;
|
|
||||||
|
|
||||||
// Create status indicator based on LoRA states
|
|
||||||
let statusHTML = '';
|
let statusHTML = '';
|
||||||
if (totalCount > 0) {
|
if (totalCount > 0) {
|
||||||
if (allLorasAvailable && deletedLorasCount === 0) {
|
if (allLorasAvailable && deletedLorasCount === 0) {
|
||||||
// All LoRAs are available
|
|
||||||
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
||||||
} else if (missingLorasCount > 0) {
|
} else if (missingLorasCount > 0) {
|
||||||
// Some LoRAs are missing (prioritize showing missing over deleted)
|
|
||||||
statusHTML = `<div class="recipe-status missing">
|
statusHTML = `<div class="recipe-status missing">
|
||||||
<i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing
|
<i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing
|
||||||
<div class="missing-tooltip">Click to download missing LoRAs</div>
|
<div class="missing-tooltip">Click to download missing LoRAs</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (deletedLorasCount > 0 && missingLorasCount === 0) {
|
} else if (deletedLorasCount > 0 && missingLorasCount === 0) {
|
||||||
// Some LoRAs are deleted but none are missing
|
|
||||||
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle"></i> ${deletedLorasCount} deleted</div>`;
|
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle"></i> ${deletedLorasCount} deleted</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
||||||
|
|
||||||
// Add event listeners for buttons and status indicators
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Set up click handler for View LoRAs button
|
|
||||||
const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
|
const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
|
||||||
if (viewRecipeLorasBtn) {
|
if (viewRecipeLorasBtn) {
|
||||||
viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
|
viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add click handler for missing LoRAs status
|
|
||||||
const missingStatus = document.querySelector('.recipe-status.missing');
|
const missingStatus = document.querySelector('.recipe-status.missing');
|
||||||
if (missingStatus && missingLorasCount > 0) {
|
if (missingStatus && missingLorasCount > 0) {
|
||||||
missingStatus.classList.add('clickable');
|
missingStatus.classList.add('clickable');
|
||||||
@@ -425,13 +673,12 @@ class RecipeModal {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lorasListElement && recipe.loras && recipe.loras.length > 0) {
|
if (lorasListElement && loras.length > 0) {
|
||||||
lorasListElement.innerHTML = recipe.loras.map(lora => {
|
lorasListElement.innerHTML = loras.map(lora => {
|
||||||
const existsLocally = lora.inLibrary;
|
const existsLocally = lora.inLibrary;
|
||||||
const isDeleted = lora.isDeleted;
|
const isDeleted = lora.isDeleted;
|
||||||
const localPath = lora.localPath || '';
|
const localPath = lora.localPath || '';
|
||||||
|
|
||||||
// Create status badge based on LoRA state
|
|
||||||
let localStatus;
|
let localStatus;
|
||||||
if (existsLocally) {
|
if (existsLocally) {
|
||||||
localStatus = `
|
localStatus = `
|
||||||
@@ -441,7 +688,7 @@ class RecipeModal {
|
|||||||
</div>`;
|
</div>`;
|
||||||
} else if (isDeleted) {
|
} else if (isDeleted) {
|
||||||
localStatus = `
|
localStatus = `
|
||||||
<div class="deleted-badge reconnectable" data-lora-index="${recipe.loras.indexOf(lora)}">
|
<div class="deleted-badge reconnectable" data-lora-index="${loras.indexOf(lora)}">
|
||||||
<span class="badge-text"><i class="fas fa-trash-alt"></i> Deleted</span>
|
<span class="badge-text"><i class="fas fa-trash-alt"></i> Deleted</span>
|
||||||
<div class="reconnect-tooltip">Click to reconnect with a local LoRA</div>
|
<div class="reconnect-tooltip">Click to reconnect with a local LoRA</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -452,7 +699,6 @@ class RecipeModal {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if preview is a video
|
|
||||||
const isPreviewVideo = lora.preview_url && lora.preview_url.toLowerCase().endsWith('.mp4');
|
const isPreviewVideo = lora.preview_url && lora.preview_url.toLowerCase().endsWith('.mp4');
|
||||||
const previewMedia = isPreviewVideo ?
|
const previewMedia = isPreviewVideo ?
|
||||||
`<video class="thumbnail-video" autoplay loop muted playsinline>
|
`<video class="thumbnail-video" autoplay loop muted playsinline>
|
||||||
@@ -460,7 +706,6 @@ class RecipeModal {
|
|||||||
</video>` :
|
</video>` :
|
||||||
`<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview">`;
|
`<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview">`;
|
||||||
|
|
||||||
// Determine CSS class based on LoRA state
|
|
||||||
let loraItemClass = 'recipe-lora-item';
|
let loraItemClass = 'recipe-lora-item';
|
||||||
if (existsLocally) {
|
if (existsLocally) {
|
||||||
loraItemClass += ' exists-locally';
|
loraItemClass += ' exists-locally';
|
||||||
@@ -471,7 +716,7 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="${loraItemClass}" data-lora-index="${recipe.loras.indexOf(lora)}">
|
<div class="${loraItemClass}" data-lora-index="${loras.indexOf(lora)}">
|
||||||
<div class="recipe-lora-thumbnail">
|
<div class="recipe-lora-thumbnail">
|
||||||
${previewMedia}
|
${previewMedia}
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +730,7 @@ class RecipeModal {
|
|||||||
<div class="recipe-lora-weight">Weight: ${lora.strength || 1.0}</div>
|
<div class="recipe-lora-weight">Weight: ${lora.strength || 1.0}</div>
|
||||||
${lora.baseModel ? `<div class="base-model">${lora.baseModel}</div>` : ''}
|
${lora.baseModel ? `<div class="base-model">${lora.baseModel}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="lora-reconnect-container" data-lora-index="${recipe.loras.indexOf(lora)}">
|
<div class="lora-reconnect-container" data-lora-index="${loras.indexOf(lora)}">
|
||||||
<div class="reconnect-instructions">
|
<div class="reconnect-instructions">
|
||||||
<p>Enter LoRA Syntax or Name to Reconnect:</p>
|
<p>Enter LoRA Syntax or Name to Reconnect:</p>
|
||||||
<small>Example: <code><lora:Boris_Vallejo_BV_flux_D:1></code> or just <code>Boris_Vallejo_BV_flux_D</code></small>
|
<small>Example: <code><lora:Boris_Vallejo_BV_flux_D:1></code> or just <code>Boris_Vallejo_BV_flux_D</code></small>
|
||||||
@@ -503,15 +748,12 @@ class RecipeModal {
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Add event listeners for reconnect functionality
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setupReconnectButtons();
|
this.setupReconnectButtons();
|
||||||
this.setupLoraItemsClickable();
|
this.setupLoraItemsClickable();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// Generate recipe syntax for copy button (this is now a placeholder, actual syntax will be fetched from the API)
|
|
||||||
this.recipeLorasSyntax = '';
|
this.recipeLorasSyntax = '';
|
||||||
|
|
||||||
} else if (lorasListElement) {
|
} else if (lorasListElement) {
|
||||||
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
||||||
this.recipeLorasSyntax = '';
|
this.recipeLorasSyntax = '';
|
||||||
@@ -522,9 +764,31 @@ class RecipeModal {
|
|||||||
const hasLoraItems = lorasListElement && lorasListElement.querySelector('.recipe-lora-item');
|
const hasLoraItems = lorasListElement && lorasListElement.querySelector('.recipe-lora-item');
|
||||||
resourceDivider.style.display = hasCheckpoint && hasLoraItems ? 'block' : 'none';
|
resourceDivider.style.display = hasCheckpoint && hasLoraItems ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show the modal
|
updateSourceUrlDisplay(sourcePath, options = {}) {
|
||||||
modalManager.showModal('recipeModal');
|
const sourceUrlContainer = document.querySelector('.source-url-container');
|
||||||
|
const sourceUrlEditor = document.querySelector('.source-url-editor');
|
||||||
|
if (!sourceUrlContainer || !sourceUrlEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
|
||||||
|
const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
|
||||||
|
if (!sourceUrlText || !sourceUrlInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSourcePath = typeof sourcePath === 'string' ? sourcePath.trim() : '';
|
||||||
|
const isValidUrl = normalizedSourcePath.startsWith('http://') || normalizedSourcePath.startsWith('https://');
|
||||||
|
|
||||||
|
sourceUrlText.textContent = normalizedSourcePath || 'No source URL';
|
||||||
|
sourceUrlText.title = normalizedSourcePath
|
||||||
|
? (isValidUrl ? 'Click to open source URL' : 'No valid URL')
|
||||||
|
: 'No valid URL';
|
||||||
|
if (options.forceInputSync || !sourceUrlEditor.classList.contains('active') || !this.sourceUrlEditState.isDirty) {
|
||||||
|
sourceUrlInput.value = normalizedSourcePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title editing methods
|
// Title editing methods
|
||||||
@@ -535,6 +799,7 @@ class RecipeModal {
|
|||||||
const editor = titleContainer.querySelector('#recipeTitleEditor');
|
const editor = titleContainer.querySelector('#recipeTitleEditor');
|
||||||
editor.classList.add('active');
|
editor.classList.add('active');
|
||||||
const input = editor.querySelector('input');
|
const input = editor.querySelector('input');
|
||||||
|
input.oninput = () => this.markFieldDirty('title');
|
||||||
input.focus();
|
input.focus();
|
||||||
input.select();
|
input.select();
|
||||||
}
|
}
|
||||||
@@ -553,19 +818,23 @@ class RecipeModal {
|
|||||||
titleContainer.querySelector('.content-text').textContent = newTitle;
|
titleContainer.querySelector('.content-text').textContent = newTitle;
|
||||||
|
|
||||||
// Update the recipe on the server
|
// Update the recipe on the server
|
||||||
updateRecipeMetadata(this.filePath, { title: newTitle })
|
updateRecipeMetadata(this.filePath, { title: newTitle }, this.getMetadataUpdateOptions())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Show success toast
|
// Show success toast
|
||||||
showToast('toast.recipes.nameUpdated', {}, 'success');
|
showToast('toast.recipes.nameUpdated', {}, 'success');
|
||||||
|
|
||||||
// Update the current recipe object
|
// Update the current recipe object
|
||||||
this.currentRecipe.title = newTitle;
|
this.currentRecipe.title = newTitle;
|
||||||
|
this.commitField('title');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Error is handled in the API function
|
// Error is handled in the API function
|
||||||
// Reset the UI if needed
|
// Reset the UI if needed
|
||||||
titleContainer.querySelector('.content-text').textContent = this.currentRecipe.title || '';
|
titleContainer.querySelector('.content-text').textContent = this.currentRecipe.title || '';
|
||||||
|
this.clearFieldDirty('title');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.clearFieldDirty('title');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
@@ -581,6 +850,7 @@ class RecipeModal {
|
|||||||
const editor = titleContainer.querySelector('#recipeTitleEditor');
|
const editor = titleContainer.querySelector('#recipeTitleEditor');
|
||||||
const input = editor.querySelector('input');
|
const input = editor.querySelector('input');
|
||||||
input.value = this.currentRecipe.title || '';
|
input.value = this.currentRecipe.title || '';
|
||||||
|
this.clearFieldDirty('title');
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
editor.classList.remove('active');
|
editor.classList.remove('active');
|
||||||
@@ -596,6 +866,7 @@ class RecipeModal {
|
|||||||
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
||||||
editor.classList.add('active');
|
editor.classList.add('active');
|
||||||
const input = editor.querySelector('input');
|
const input = editor.querySelector('input');
|
||||||
|
input.oninput = () => this.markFieldDirty('tags');
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,20 +894,24 @@ class RecipeModal {
|
|||||||
|
|
||||||
if (tagsChanged) {
|
if (tagsChanged) {
|
||||||
// Update the recipe on the server
|
// Update the recipe on the server
|
||||||
updateRecipeMetadata(this.filePath, { tags: newTags })
|
updateRecipeMetadata(this.filePath, { tags: newTags }, this.getMetadataUpdateOptions())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Show success toast
|
// Show success toast
|
||||||
showToast('toast.recipes.tagsUpdated', {}, 'success');
|
showToast('toast.recipes.tagsUpdated', {}, 'success');
|
||||||
|
|
||||||
// Update the current recipe object
|
// Update the current recipe object
|
||||||
this.currentRecipe.tags = newTags;
|
this.currentRecipe.tags = newTags;
|
||||||
|
this.commitField('tags');
|
||||||
|
|
||||||
// Update tags in the UI
|
// Update tags in the UI
|
||||||
this.updateTagsDisplay(tagsContainer, newTags);
|
this.updateTagsDisplay(tagsContainer, newTags);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Error is handled in the API function
|
// Error is handled in the API function
|
||||||
|
this.clearFieldDirty('tags');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.clearFieldDirty('tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
@@ -708,6 +983,7 @@ class RecipeModal {
|
|||||||
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
const editor = tagsContainer.querySelector('#recipeTagsEditor');
|
||||||
const input = editor.querySelector('input');
|
const input = editor.querySelector('input');
|
||||||
input.value = this.currentRecipe.tags ? this.currentRecipe.tags.join(', ') : '';
|
input.value = this.currentRecipe.tags ? this.currentRecipe.tags.join(', ') : '';
|
||||||
|
this.clearFieldDirty('tags');
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
editor.classList.remove('active');
|
editor.classList.remove('active');
|
||||||
@@ -748,6 +1024,7 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
|
input.addEventListener('input', () => this.markFieldDirty(config.field));
|
||||||
input.addEventListener('keydown', (event) => {
|
input.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -843,6 +1120,7 @@ class RecipeModal {
|
|||||||
const currentValue = currentGenParams[config.field] || '';
|
const currentValue = currentGenParams[config.field] || '';
|
||||||
|
|
||||||
if (nextValue === currentValue) {
|
if (nextValue === currentValue) {
|
||||||
|
this.clearFieldDirty(config.field);
|
||||||
this.hidePromptEditor(config);
|
this.hidePromptEditor(config);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -857,14 +1135,17 @@ class RecipeModal {
|
|||||||
...promptState,
|
...promptState,
|
||||||
isSaving: true,
|
isSaving: true,
|
||||||
};
|
};
|
||||||
await updateRecipeMetadata(this.filePath, { gen_params: nextGenParams });
|
await updateRecipeMetadata(this.filePath, { gen_params: nextGenParams }, this.getMetadataUpdateOptions());
|
||||||
this.currentRecipe.gen_params = nextGenParams;
|
this.currentRecipe.gen_params = nextGenParams;
|
||||||
this.renderPromptContent(content, nextValue, config.placeholder);
|
this.renderPromptContent(content, nextValue, config.placeholder);
|
||||||
showToast(config.successKey, {}, 'success', config.successFallback);
|
showToast(config.successKey, {}, 'success', config.successFallback);
|
||||||
|
this.commitField(config.field);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.renderPromptContent(content, currentValue, config.placeholder);
|
this.renderPromptContent(content, currentValue, config.placeholder);
|
||||||
input.value = currentValue;
|
input.value = currentValue;
|
||||||
|
this.clearFieldDirty(config.field);
|
||||||
} finally {
|
} finally {
|
||||||
|
this.clearFieldDirty(config.field);
|
||||||
this.hidePromptEditor(config);
|
this.hidePromptEditor(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,10 +1153,10 @@ class RecipeModal {
|
|||||||
cancelPromptEdit(config) {
|
cancelPromptEdit(config) {
|
||||||
const input = document.getElementById(config.inputId);
|
const input = document.getElementById(config.inputId);
|
||||||
if (input) {
|
if (input) {
|
||||||
const initialValue = this.promptEditorState[config.field]?.initialValue;
|
input.value = this.currentRecipe?.gen_params?.[config.field] || '';
|
||||||
input.value = initialValue ?? (this.currentRecipe?.gen_params?.[config.field] || '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.clearFieldDirty(config.field);
|
||||||
this.hidePromptEditor(config);
|
this.hidePromptEditor(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,11 +1199,16 @@ class RecipeModal {
|
|||||||
sourceUrlInput.focus();
|
sourceUrlInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sourceUrlInput.addEventListener('input', () => {
|
||||||
|
this.sourceUrlEditState.isDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
// Cancel editing
|
// Cancel editing
|
||||||
sourceUrlCancelBtn.addEventListener('click', () => {
|
sourceUrlCancelBtn.addEventListener('click', () => {
|
||||||
sourceUrlEditor.classList.remove('active');
|
sourceUrlEditor.classList.remove('active');
|
||||||
sourceUrlContainer.classList.remove('hide');
|
sourceUrlContainer.classList.remove('hide');
|
||||||
sourceUrlInput.value = this.currentRecipe.source_path || '';
|
this.updateSourceUrlDisplay(this.currentRecipe.source_path || '', { forceInputSync: true });
|
||||||
|
this.clearFieldDirty('source_path');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save new source URL
|
// Save new source URL
|
||||||
@@ -930,23 +1216,24 @@ class RecipeModal {
|
|||||||
const newSourceUrl = sourceUrlInput.value.trim();
|
const newSourceUrl = sourceUrlInput.value.trim();
|
||||||
if (newSourceUrl !== this.currentRecipe.source_path) {
|
if (newSourceUrl !== this.currentRecipe.source_path) {
|
||||||
// Update the recipe on the server
|
// Update the recipe on the server
|
||||||
updateRecipeMetadata(this.filePath, { source_path: newSourceUrl })
|
updateRecipeMetadata(this.filePath, { source_path: newSourceUrl }, this.getMetadataUpdateOptions())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Show success toast
|
// Show success toast
|
||||||
showToast('toast.recipes.sourceUrlUpdated', {}, 'success');
|
showToast('toast.recipes.sourceUrlUpdated', {}, 'success');
|
||||||
|
|
||||||
// Update source URL in the UI
|
// Update source URL in the UI
|
||||||
sourceUrlText.textContent = newSourceUrl || 'No source URL';
|
this.commitField('source_path');
|
||||||
sourceUrlText.title = newSourceUrl && (newSourceUrl.startsWith('http://') ||
|
this.updateSourceUrlDisplay(newSourceUrl, { forceInputSync: true });
|
||||||
newSourceUrl.startsWith('https://')) ?
|
|
||||||
'Click to open source URL' : 'No valid URL';
|
|
||||||
|
|
||||||
// Update the current recipe object
|
// Update the current recipe object
|
||||||
this.currentRecipe.source_path = newSourceUrl;
|
this.currentRecipe.source_path = newSourceUrl;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
// Error is handled in the API function
|
// Error is handled in the API function
|
||||||
|
this.clearFieldDirty('source_path');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.clearFieldDirty('source_path');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
@@ -1286,7 +1573,7 @@ class RecipeModal {
|
|||||||
this.showRecipeDetails(this.currentRecipe);
|
this.showRecipeDetails(this.currentRecipe);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
state.virtualScroller.updateSingleItem(this.currentRecipe.file_path, {
|
state.virtualScroller.updateSingleItem(this.listFilePath || this.currentRecipe.file_path, {
|
||||||
loras: this.currentRecipe.loras
|
loras: this.currentRecipe.loras
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ const loadingManagerMock = vi.hoisted(() => ({
|
|||||||
showSimpleLoading: vi.fn(),
|
showSimpleLoading: vi.fn(),
|
||||||
hide: vi.fn(),
|
hide: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
const virtualScrollerMock = vi.hoisted(() => ({
|
||||||
|
updateSingleItem: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => {
|
vi.mock('../../../static/js/utils/uiHelpers.js', () => {
|
||||||
return {
|
return {
|
||||||
@@ -20,12 +23,13 @@ vi.mock('../../../static/js/state/index.js', () => {
|
|||||||
return {
|
return {
|
||||||
state: {
|
state: {
|
||||||
loadingManager: loadingManagerMock,
|
loadingManager: loadingManagerMock,
|
||||||
|
virtualScroller: virtualScrollerMock,
|
||||||
},
|
},
|
||||||
getCurrentPageState: vi.fn(),
|
getCurrentPageState: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { RecipeSidebarApiClient } from '../../../static/js/api/recipeApi.js';
|
import { RecipeSidebarApiClient, fetchRecipeDetails, updateRecipeMetadata } from '../../../static/js/api/recipeApi.js';
|
||||||
|
|
||||||
describe('RecipeSidebarApiClient bulk operations', () => {
|
describe('RecipeSidebarApiClient bulk operations', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -111,4 +115,37 @@ describe('RecipeSidebarApiClient bulk operations', () => {
|
|||||||
});
|
});
|
||||||
expect(loadingManagerMock.hide).toHaveBeenCalled();
|
expect(loadingManagerMock.hide).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('encodes recipe IDs when fetching recipe details', async () => {
|
||||||
|
global.fetch.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ id: 'abc' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchRecipeDetails('recipe#1?name=foo%bar');
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/lm/recipe/recipe%231%3Fname%3Dfoo%25bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the virtual scroller using the original list path when provided', async () => {
|
||||||
|
global.fetch.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateRecipeMetadata(
|
||||||
|
'/recipes/new-folder/recipe#1.webp',
|
||||||
|
{ title: 'Updated Title' },
|
||||||
|
{ listFilePath: '/recipes/old-folder/recipe#1.webp' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'/api/lm/recipe/recipe%231/update',
|
||||||
|
expect.objectContaining({ method: 'PUT' })
|
||||||
|
);
|
||||||
|
expect(virtualScrollerMock.updateSingleItem).toHaveBeenCalledWith(
|
||||||
|
'/recipes/old-folder/recipe#1.webp',
|
||||||
|
{ title: 'Updated Title' }
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -358,6 +358,133 @@ async def test_get_recipe_by_id_handles_non_dict_checkpoint(recipe_scanner):
|
|||||||
assert recipe["checkpoint"]["file_name"] == "by-id"
|
assert recipe["checkpoint"]["file_name"] == "by-id"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_recipe_by_id_merges_recipe_json_details(recipe_scanner):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
recipes_dir = Path(scanner.recipes_dir)
|
||||||
|
recipe_id = "hydrate-me"
|
||||||
|
recipe_json_path = recipes_dir / f"{recipe_id}.recipe.json"
|
||||||
|
recipe_json_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/hydrate-me.png",
|
||||||
|
"title": "Hydrated Recipe",
|
||||||
|
"source_path": "https://example.com/source",
|
||||||
|
"gen_params": {
|
||||||
|
"prompt": "prompt from json",
|
||||||
|
"negative_prompt": "negative from json",
|
||||||
|
},
|
||||||
|
"loras": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
scanner._cache.raw_data = [
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/hydrate-me.png",
|
||||||
|
"title": "Cached Recipe",
|
||||||
|
"folder": "",
|
||||||
|
"modified": 0.0,
|
||||||
|
"created_date": 0.0,
|
||||||
|
"loras": [],
|
||||||
|
"gen_params": {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
recipe = await scanner.get_recipe_by_id(recipe_id)
|
||||||
|
|
||||||
|
assert recipe is not None
|
||||||
|
assert recipe["title"] == "Hydrated Recipe"
|
||||||
|
assert recipe["source_path"] == "https://example.com/source"
|
||||||
|
assert recipe["gen_params"]["prompt"] == "prompt from json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_recipe_by_id_prefers_json_file_path(recipe_scanner):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
recipes_dir = Path(scanner.recipes_dir)
|
||||||
|
recipe_id = "move-me"
|
||||||
|
recipe_json_path = recipes_dir / f"{recipe_id}.recipe.json"
|
||||||
|
recipe_json_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/new-location.png",
|
||||||
|
"title": "Moved Recipe",
|
||||||
|
"source_path": "https://example.com/moved",
|
||||||
|
"gen_params": {},
|
||||||
|
"loras": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
scanner._cache.raw_data = [
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/old-location.png",
|
||||||
|
"title": "Cached Title",
|
||||||
|
"folder": "",
|
||||||
|
"modified": 0.0,
|
||||||
|
"created_date": 0.0,
|
||||||
|
"loras": [],
|
||||||
|
"gen_params": {},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
recipe = await scanner.get_recipe_by_id(recipe_id)
|
||||||
|
|
||||||
|
assert recipe is not None
|
||||||
|
assert recipe["file_path"] == "/tmp/new-location.png"
|
||||||
|
assert recipe["title"] == "Moved Recipe"
|
||||||
|
assert recipe["source_path"] == "https://example.com/moved"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_recipe_by_id_drops_deleted_optional_json_fields(recipe_scanner):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
recipes_dir = Path(scanner.recipes_dir)
|
||||||
|
recipe_id = "drop-optional-fields"
|
||||||
|
recipe_json_path = recipes_dir / f"{recipe_id}.recipe.json"
|
||||||
|
recipe_json_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/drop-optional-fields.png",
|
||||||
|
"title": "Trimmed Recipe",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
scanner._cache.raw_data = [
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/drop-optional-fields.png",
|
||||||
|
"title": "Cached Recipe",
|
||||||
|
"folder": "",
|
||||||
|
"modified": 0.0,
|
||||||
|
"created_date": 0.0,
|
||||||
|
"source_path": "https://example.com/stale-source",
|
||||||
|
"checkpoint": {"name": "stale-checkpoint.safetensors"},
|
||||||
|
"loras": [{"modelName": "stale-lora"}],
|
||||||
|
"gen_params": {"prompt": "stale prompt"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
recipe = await scanner.get_recipe_by_id(recipe_id)
|
||||||
|
|
||||||
|
assert recipe is not None
|
||||||
|
assert recipe["title"] == "Trimmed Recipe"
|
||||||
|
assert "source_path" not in recipe
|
||||||
|
assert "checkpoint" not in recipe
|
||||||
|
assert "gen_params" not in recipe
|
||||||
|
assert "loras" not in recipe
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_paginated_data_filters_by_checkpoint_hash(recipe_scanner):
|
async def test_get_paginated_data_filters_by_checkpoint_hash(recipe_scanner):
|
||||||
scanner, _ = recipe_scanner
|
scanner, _ = recipe_scanner
|
||||||
|
|||||||
Reference in New Issue
Block a user