diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py
index a6842643..f86b4fab 100644
--- a/py/routes/misc_routes.py
+++ b/py/routes/misc_routes.py
@@ -5,6 +5,8 @@ import json
import time
import aiohttp
import re
+import subprocess
+import sys
from server import PromptServer # type: ignore
from aiohttp import web
from ..services.settings_manager import settings
@@ -55,7 +57,10 @@ class MiscRoutes:
# Lora code update endpoint
app.router.add_post('/api/update-lora-code', MiscRoutes.update_lora_code)
-
+
+ # Add new route for opening example images folder
+ app.router.add_post('/api/open-example-images-folder', MiscRoutes.open_example_images_folder)
+
@staticmethod
async def clear_cache(request):
"""Clear all cache files from the cache folder"""
@@ -864,3 +869,63 @@ class MiscRoutes:
'success': False,
'error': str(e)
}, status=500)
+
+ @staticmethod
+ async def open_example_images_folder(request):
+ """
+ Open the example images folder for a specific model
+
+ Expects a JSON body with:
+ {
+ "model_hash": "sha256_hash" # SHA256 hash of the model
+ }
+ """
+ try:
+ # Parse the request body
+ data = await request.json()
+ model_hash = data.get('model_hash')
+
+ if not model_hash:
+ return web.json_response({
+ 'success': False,
+ 'error': 'Missing model_hash parameter'
+ }, status=400)
+
+ # Get the example images path from settings
+ example_images_path = settings.get('example_images_path')
+ if not example_images_path:
+ return web.json_response({
+ 'success': False,
+ 'error': 'No example images path configured. Please set it in the settings panel first.'
+ }, status=400)
+
+ # Construct the folder path for this model
+ model_folder = os.path.join(example_images_path, model_hash)
+
+ # Check if the folder exists
+ if not os.path.exists(model_folder):
+ return web.json_response({
+ 'success': False,
+ 'error': 'No example images found for this model. Download example images first.'
+ }, status=404)
+
+ # Open the folder in the file explorer
+ if os.name == 'nt': # Windows
+ os.startfile(model_folder)
+ elif os.name == 'posix': # macOS and Linux
+ if sys.platform == 'darwin': # macOS
+ subprocess.Popen(['open', model_folder])
+ else: # Linux
+ subprocess.Popen(['xdg-open', model_folder])
+
+ return web.json_response({
+ 'success': True,
+ 'message': f'Opened example images folder for model {model_hash}'
+ })
+
+ except Exception as e:
+ logger.error(f"Failed to open example images folder: {e}", exc_info=True)
+ return web.json_response({
+ 'success': False,
+ 'error': str(e)
+ }, status=500)
diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js
index 4b9b7d20..f494d6f9 100644
--- a/static/js/components/CheckpointCard.js
+++ b/static/js/components/CheckpointCard.js
@@ -1,4 +1,4 @@
-import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
+import { showToast, copyToClipboard, openExampleImagesFolder } from '../utils/uiHelpers.js';
import { state } from '../state/index.js';
import { showCheckpointModal } from './checkpointModal/index.js';
import { NSFW_LEVELS } from '../utils/constants.js';
@@ -115,8 +115,8 @@ export function createCheckpointCard(checkpoint) {
${checkpoint.model_name}
-
+
@@ -272,6 +272,12 @@ export function createCheckpointCard(checkpoint) {
replaceCheckpointPreview(checkpoint.file_path);
});
+ // Open example images folder button click event
+ card.querySelector('.fa-folder-open')?.addEventListener('click', e => {
+ e.stopPropagation();
+ openExampleImagesFolder(checkpoint.sha256);
+ });
+
// Add autoplayOnHover handlers for video elements if needed
const videoElement = card.querySelector('video');
if (videoElement && autoplayOnHover) {
diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js
index e5e5aea0..0945901f 100644
--- a/static/js/components/ContextMenu/CheckpointContextMenu.js
+++ b/static/js/components/ContextMenu/CheckpointContextMenu.js
@@ -1,6 +1,6 @@
import { BaseContextMenu } from './BaseContextMenu.js';
-import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/checkpointApi.js';
-import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js';
+import { refreshSingleCheckpointMetadata, saveModelMetadata, replaceCheckpointPreview } from '../../api/checkpointApi.js';
+import { showToast, getNSFWLevelName, openExampleImagesFolder } from '../../utils/uiHelpers.js';
import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal } from '../../utils/modalUtils.js';
@@ -23,10 +23,12 @@ export class CheckpointContextMenu extends BaseContextMenu {
this.currentCard.click();
break;
case 'preview':
- // Replace checkpoint preview
- if (this.currentCard.querySelector('.fa-image')) {
- this.currentCard.querySelector('.fa-image').click();
- }
+ // Open example images folder instead of replacing preview
+ openExampleImagesFolder(this.currentCard.dataset.sha256);
+ break;
+ case 'replace-preview':
+ // Add new action for replacing preview images
+ replaceCheckpointPreview(this.currentCard.dataset.filepath);
break;
case 'civitai':
// Open civitai page
diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js
index ed155ab5..c2cdbc8e 100644
--- a/static/js/components/ContextMenu/LoraContextMenu.js
+++ b/static/js/components/ContextMenu/LoraContextMenu.js
@@ -1,6 +1,6 @@
import { BaseContextMenu } from './BaseContextMenu.js';
-import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi.js';
-import { showToast, getNSFWLevelName, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHelpers.js';
+import { refreshSingleLoraMetadata, saveModelMetadata, replacePreview } from '../../api/loraApi.js';
+import { showToast, getNSFWLevelName, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js';
import { NSFW_LEVELS } from '../../utils/constants.js';
import { getStorageItem } from '../../utils/storageHelpers.js';
import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js';
@@ -47,7 +47,12 @@ export class LoraContextMenu extends BaseContextMenu {
this.sendLoraToWorkflow(true);
break;
case 'preview':
- this.currentCard.querySelector('.fa-image')?.click();
+ // Open example images folder instead of showing preview image dialog
+ openExampleImagesFolder(this.currentCard.dataset.sha256);
+ break;
+ case 'replace-preview':
+ // Add a new action for replacing preview images
+ replacePreview(this.currentCard.dataset.filepath);
break;
case 'delete':
// Call showDeleteModal directly instead of clicking the trash button
diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js
index bba68a54..e1c53375 100644
--- a/static/js/components/LoraCard.js
+++ b/static/js/components/LoraCard.js
@@ -1,4 +1,4 @@
-import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
+import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../utils/uiHelpers.js';
import { state } from '../state/index.js';
import { showLoraModal } from './loraModal/index.js';
import { bulkManager } from '../managers/BulkManager.js';
@@ -69,6 +69,12 @@ function handleLoraCardEvent(event) {
return;
}
+ if (event.target.closest('.fa-folder-open')) {
+ event.stopPropagation();
+ openExampleImagesFolder(card.dataset.sha256);
+ return;
+ }
+
// If no specific element was clicked, handle the card click (show modal or toggle selection)
if (state.bulkMode) {
// Toggle selection using the bulk manager
@@ -300,14 +306,14 @@ export function createLoraCard(lora) {
${lora.model_name}
-
+
`;
-
+
// Add a special class for virtual scroll positioning if needed
if (state.virtualScroller) {
card.classList.add('virtual-scroll-item');
diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js
index 37c4ef65..c86e3072 100644
--- a/static/js/utils/uiHelpers.js
+++ b/static/js/utils/uiHelpers.js
@@ -419,4 +419,36 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax
showToast(`Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error');
return false;
}
+}
+
+/**
+ * Opens the example images folder for a specific model
+ * @param {string} modelHash - The SHA256 hash of the model
+ */
+export async function openExampleImagesFolder(modelHash) {
+ try {
+ const response = await fetch('/api/open-example-images-folder', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ model_hash: modelHash
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast('Opening example images folder', 'success');
+ return true;
+ } else {
+ showToast(result.error || 'Failed to open example images folder', 'error');
+ return false;
+ }
+ } catch (error) {
+ console.error('Failed to open example images folder:', error);
+ showToast('Failed to open example images folder', 'error');
+ return false;
+ }
}
\ No newline at end of file
diff --git a/templates/checkpoints.html b/templates/checkpoints.html
index 5eb3371d..a455030f 100644
--- a/templates/checkpoints.html
+++ b/templates/checkpoints.html
@@ -19,7 +19,8 @@
-
+
+
diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html
index c60debd1..8f600a4d 100644
--- a/templates/components/context_menu.html
+++ b/templates/components/context_menu.html
@@ -18,6 +18,9 @@
Send to Workflow (Replace)
+