feat: add recipe root directory and move recipe endpoints

- Add GET /api/lm/recipes/roots endpoint to retrieve recipe root directories
- Add POST /api/lm/recipe/move endpoint to move recipes between directories
- Register new endpoints in route definitions
- Implement error handling for both new endpoints with proper status codes
- Enable recipe management operations for better file organization
This commit is contained in:
Will Miao
2025-11-25 17:41:24 +08:00
parent 67fb205b43
commit 3f646aa0c9
21 changed files with 501 additions and 110 deletions

View File

@@ -7,19 +7,28 @@ const RECIPE_ENDPOINTS = {
detail: '/api/lm/recipe',
scan: '/api/lm/recipes/scan',
update: '/api/lm/recipe',
roots: '/api/lm/recipes/roots',
folders: '/api/lm/recipes/folders',
folderTree: '/api/lm/recipes/folder-tree',
unifiedFolderTree: '/api/lm/recipes/unified-folder-tree',
move: '/api/lm/recipe/move',
};
const RECIPE_SIDEBAR_CONFIG = {
config: {
displayName: 'Recipes',
supportsMove: false,
supportsMove: true,
},
endpoints: RECIPE_ENDPOINTS,
};
function extractRecipeId(filePath) {
if (!filePath) return null;
const basename = filePath.split('/').pop().split('\\').pop();
const dotIndex = basename.lastIndexOf('.');
return dotIndex > 0 ? basename.substring(0, dotIndex) : basename;
}
/**
* Fetch recipes with pagination for virtual scrolling
* @param {number} page - Page number to fetch
@@ -302,8 +311,10 @@ export async function updateRecipeMetadata(filePath, updates) {
state.loadingManager.showSimpleLoading('Saving metadata...');
// Extract recipeId from filePath (basename without extension)
const basename = filePath.split('/').pop().split('\\').pop();
const recipeId = basename.substring(0, basename.lastIndexOf('.'));
const recipeId = extractRecipeId(filePath);
if (!recipeId) {
throw new Error('Unable to determine recipe ID');
}
const response = await fetch(`${RECIPE_ENDPOINTS.update}/${recipeId}/update`, {
method: 'PUT',
@@ -345,6 +356,14 @@ export class RecipeSidebarApiClient {
return response.json();
}
async fetchModelRoots() {
const response = await fetch(this.apiConfig.endpoints.roots);
if (!response.ok) {
throw new Error('Failed to fetch recipe roots');
}
return response.json();
}
async fetchModelFolders() {
const response = await fetch(this.apiConfig.endpoints.folders);
if (!response.ok) {
@@ -353,11 +372,69 @@ export class RecipeSidebarApiClient {
return response.json();
}
async moveBulkModels() {
throw new Error('Recipe move operations are not supported.');
async moveBulkModels(filePaths, targetPath) {
const results = [];
for (const path of filePaths) {
try {
const result = await this.moveSingleModel(path, targetPath);
results.push({
original_file_path: path,
new_file_path: result?.new_file_path,
success: !!result,
message: result?.message,
});
} catch (error) {
results.push({
original_file_path: path,
new_file_path: null,
success: false,
message: error.message,
});
}
}
return results;
}
async moveSingleModel() {
throw new Error('Recipe move operations are not supported.');
async moveSingleModel(filePath, targetPath) {
if (!this.apiConfig.config.supportsMove) {
showToast('toast.api.moveNotSupported', { type: this.apiConfig.config.displayName }, 'warning');
return null;
}
const recipeId = extractRecipeId(filePath);
if (!recipeId) {
showToast('toast.api.moveFailed', { message: 'Recipe ID missing' }, 'error');
return null;
}
const response = await fetch(this.apiConfig.endpoints.move, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipe_id: recipeId,
target_path: targetPath,
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.error || `Failed to move ${this.apiConfig.config.displayName}`);
}
if (result.message) {
showToast('toast.api.moveInfo', { message: result.message }, 'info');
} else {
showToast('toast.api.moveSuccess', { type: this.apiConfig.config.displayName }, 'success');
}
return {
original_file_path: result.original_file_path || filePath,
new_file_path: result.new_file_path || filePath,
folder: result.folder || '',
message: result.message,
};
}
}

View File

@@ -4,6 +4,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHe
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
import { updateRecipeMetadata } from '../../api/recipeApi.js';
import { state } from '../../state/index.js';
import { moveManager } from '../../managers/MoveManager.js';
export class RecipeContextMenu extends BaseContextMenu {
constructor() {
@@ -77,6 +78,9 @@ export class RecipeContextMenu extends BaseContextMenu {
// Share recipe
this.currentCard.querySelector('.fa-share-alt')?.click();
break;
case 'move':
moveManager.showMoveModal(this.currentCard.dataset.filepath);
break;
case 'delete':
// Delete recipe
this.currentCard.querySelector('.fa-trash')?.click();

View File

@@ -3,6 +3,7 @@ import { state, getCurrentPageState } from '../state/index.js';
import { modalManager } from './ModalManager.js';
import { bulkManager } from './BulkManager.js';
import { getModelApiClient } from '../api/modelApiFactory.js';
import { RecipeSidebarApiClient } from '../api/recipeApi.js';
import { FolderTreeManager } from '../components/FolderTreeManager.js';
import { sidebarManager } from '../components/SidebarManager.js';
@@ -12,11 +13,22 @@ class MoveManager {
this.bulkFilePaths = null;
this.folderTreeManager = new FolderTreeManager();
this.initialized = false;
this.recipeApiClient = null;
// Bind methods
this.updateTargetPath = this.updateTargetPath.bind(this);
}
_getApiClient(modelType = null) {
if (state.currentPageType === 'recipes') {
if (!this.recipeApiClient) {
this.recipeApiClient = new RecipeSidebarApiClient();
}
return this.recipeApiClient;
}
return getModelApiClient(modelType);
}
initializeEventListeners() {
if (this.initialized) return;
@@ -36,7 +48,7 @@ class MoveManager {
this.currentFilePath = null;
this.bulkFilePaths = null;
const apiClient = getModelApiClient();
const apiClient = this._getApiClient(modelType);
const currentPageType = state.currentPageType;
const modelConfig = apiClient.apiConfig.config;
@@ -121,7 +133,7 @@ class MoveManager {
async initializeFolderTree() {
try {
const apiClient = getModelApiClient();
const apiClient = this._getApiClient();
// Fetch unified folder tree
const treeData = await apiClient.fetchUnifiedFolderTree();
@@ -141,7 +153,7 @@ class MoveManager {
updateTargetPath() {
const pathDisplay = document.getElementById('moveTargetPathDisplay');
const modelRoot = document.getElementById('moveModelRoot').value;
const apiClient = getModelApiClient();
const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config;
let fullPath = modelRoot || `Select a ${config.displayName.toLowerCase()} root directory`;
@@ -158,7 +170,7 @@ class MoveManager {
async moveModel() {
const selectedRoot = document.getElementById('moveModelRoot').value;
const apiClient = getModelApiClient();
const apiClient = this._getApiClient();
const config = apiClient.apiConfig.config;
if (!selectedRoot) {