mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-11 08:20:14 -03:00
feat(recipes): add prompt-aware duplicate detection toggle
This commit is contained in:
@@ -486,6 +486,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty-state hint in the duplicates view */
|
||||
.duplicates-empty-state {
|
||||
padding: 48px 16px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
font-size: 0.95em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Matching basis text in the duplicates banner */
|
||||
.duplicates-basis {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.8;
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
background: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.12);
|
||||
border: 1px solid oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Help icon styling */
|
||||
.help-icon {
|
||||
color: var(--text-color);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Duplicates Manager Component
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { RecipeCard } from './RecipeCard.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { recreateVirtualScroll } from '../utils/infiniteScroll.js';
|
||||
@@ -10,11 +11,87 @@ export class DuplicatesManager {
|
||||
this.duplicateGroups = [];
|
||||
this.inDuplicateMode = false;
|
||||
this.selectedForDeletion = new Set();
|
||||
this._initPromptMatchToggle();
|
||||
this._initHelpTooltip();
|
||||
}
|
||||
|
||||
|
||||
_getPromptMatchPreference() {
|
||||
return localStorage.getItem('recipes_duplicates_include_prompt') === '1';
|
||||
}
|
||||
|
||||
_setPromptMatchPreference(enabled) {
|
||||
localStorage.setItem('recipes_duplicates_include_prompt', enabled ? '1' : '0');
|
||||
}
|
||||
|
||||
updateBasisDisplay() {
|
||||
const basisEl = document.getElementById('duplicatesBasis');
|
||||
const helpTextEl = document.getElementById('duplicatesHelpText');
|
||||
const checkbox = document.getElementById('promptMatchInput');
|
||||
const includePrompt = this._getPromptMatchPreference();
|
||||
if (checkbox) {
|
||||
checkbox.checked = includePrompt;
|
||||
}
|
||||
if (basisEl) {
|
||||
basisEl.textContent = translate(
|
||||
includePrompt
|
||||
? 'recipes.duplicates.basis.loraComboAndPrompt'
|
||||
: 'recipes.duplicates.basis.loraCombo'
|
||||
);
|
||||
}
|
||||
if (helpTextEl) {
|
||||
helpTextEl.textContent = translate(
|
||||
includePrompt
|
||||
? 'recipes.duplicates.basis.hintPromptIncluded'
|
||||
: 'recipes.duplicates.basis.hintLoraCombo'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_initPromptMatchToggle() {
|
||||
const checkbox = document.getElementById('promptMatchInput');
|
||||
if (!checkbox) return;
|
||||
checkbox.addEventListener('change', async (e) => {
|
||||
this._setPromptMatchPreference(e.target.checked);
|
||||
this.updateBasisDisplay();
|
||||
checkbox.disabled = true;
|
||||
try {
|
||||
await this.findDuplicates();
|
||||
} finally {
|
||||
checkbox.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_initHelpTooltip() {
|
||||
const helpIcon = document.getElementById('duplicatesHelp');
|
||||
const helpTooltip = document.getElementById('duplicatesHelpTooltip');
|
||||
if (!helpIcon || !helpTooltip) return;
|
||||
|
||||
helpIcon.addEventListener('mouseenter', () => {
|
||||
const bannerContent = helpIcon.closest('.banner-content');
|
||||
if (!bannerContent) return;
|
||||
const iconRect = helpIcon.getBoundingClientRect();
|
||||
const bannerRect = bannerContent.getBoundingClientRect();
|
||||
helpTooltip.style.display = 'block';
|
||||
helpTooltip.style.top = `${iconRect.bottom - bannerRect.top + 10}px`;
|
||||
helpTooltip.style.left = `${iconRect.left - bannerRect.left - 10}px`;
|
||||
const tooltipRect = helpTooltip.getBoundingClientRect();
|
||||
if (tooltipRect.right > window.innerWidth - 20) {
|
||||
helpTooltip.style.left = `${bannerContent.offsetWidth - tooltipRect.width - 20}px`;
|
||||
}
|
||||
});
|
||||
helpIcon.addEventListener('mouseleave', () => {
|
||||
helpTooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async findDuplicates() {
|
||||
try {
|
||||
const response = await fetch('/api/lm/recipes/find-duplicates');
|
||||
const includePrompt = this._getPromptMatchPreference();
|
||||
const endpoint = includePrompt
|
||||
? '/api/lm/recipes/find-duplicates?include_prompt=1'
|
||||
: '/api/lm/recipes/find-duplicates';
|
||||
const response = await fetch(endpoint);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to find duplicates');
|
||||
}
|
||||
@@ -28,7 +105,14 @@ export class DuplicatesManager {
|
||||
|
||||
if (this.duplicateGroups.length === 0) {
|
||||
showToast('toast.duplicates.noDuplicatesFound', { type: 'recipes' }, 'info');
|
||||
return false;
|
||||
// Keep (or enter) the duplicates view when the user is tuning
|
||||
// the matching basis, so the prompt-matching toggle stays
|
||||
// reachable; otherwise just toast and stay on the library grid.
|
||||
if (!this.inDuplicateMode && !includePrompt) {
|
||||
return false;
|
||||
}
|
||||
this.enterDuplicateMode();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.enterDuplicateMode();
|
||||
@@ -53,9 +137,14 @@ export class DuplicatesManager {
|
||||
const countSpan = document.getElementById('duplicatesCount');
|
||||
|
||||
if (banner && countSpan) {
|
||||
countSpan.textContent = `Found ${this.duplicateGroups.length} duplicate group${this.duplicateGroups.length !== 1 ? 's' : ''}`;
|
||||
countSpan.textContent = this.duplicateGroups.length === 0
|
||||
? translate('recipes.duplicates.noGroups')
|
||||
: translate('recipes.duplicates.found', { count: this.duplicateGroups.length });
|
||||
banner.style.display = 'block';
|
||||
}
|
||||
|
||||
// Restore the prompt-matching preference and show the matching basis
|
||||
this.updateBasisDisplay();
|
||||
|
||||
// Disable virtual scrolling if active
|
||||
if (state.virtualScroller) {
|
||||
@@ -113,12 +202,23 @@ export class DuplicatesManager {
|
||||
|
||||
// Clear existing content
|
||||
recipeGrid.innerHTML = '';
|
||||
|
||||
// Empty-state view: keep the banner (and the matching-basis toggle)
|
||||
// reachable when no groups match the current basis
|
||||
if (this.duplicateGroups.length === 0) {
|
||||
const emptyState = document.createElement('div');
|
||||
emptyState.className = 'duplicates-empty-state';
|
||||
emptyState.textContent = translate('recipes.duplicates.noGroups');
|
||||
recipeGrid.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render each duplicate group
|
||||
this.duplicateGroups.forEach((group, groupIndex) => {
|
||||
const groupKey = group.key;
|
||||
const groupDiv = document.createElement('div');
|
||||
groupDiv.className = 'duplicate-group';
|
||||
groupDiv.dataset.fingerprint = group.fingerprint;
|
||||
groupDiv.dataset.groupKey = groupKey;
|
||||
|
||||
// Create group header
|
||||
const header = document.createElement('div');
|
||||
@@ -126,10 +226,10 @@ export class DuplicatesManager {
|
||||
header.innerHTML = `
|
||||
<span>Duplicate Group #${groupIndex + 1} (${group.recipes.length} recipes)</span>
|
||||
<span>
|
||||
<button class="btn-select-all" onclick="recipeManager.duplicatesManager.toggleSelectAllInGroup('${group.fingerprint}')">
|
||||
<button class="btn-select-all" onclick="recipeManager.duplicatesManager.toggleSelectAllInGroup('${groupKey}')">
|
||||
Select All
|
||||
</button>
|
||||
<button class="btn-select-latest" onclick="recipeManager.duplicatesManager.selectLatestInGroup('${group.fingerprint}')">
|
||||
<button class="btn-select-latest" onclick="recipeManager.duplicatesManager.selectLatestInGroup('${groupKey}')">
|
||||
Keep Latest
|
||||
</button>
|
||||
</span>
|
||||
@@ -182,7 +282,7 @@ export class DuplicatesManager {
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'selector-checkbox';
|
||||
checkbox.dataset.recipeId = recipe.id;
|
||||
checkbox.dataset.groupFingerprint = group.fingerprint;
|
||||
checkbox.dataset.groupKey = groupKey;
|
||||
|
||||
// Check if already selected
|
||||
if (this.selectedForDeletion.has(recipe.id)) {
|
||||
@@ -244,8 +344,8 @@ export class DuplicatesManager {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAllInGroup(fingerprint) {
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-fingerprint="${fingerprint}"]`);
|
||||
toggleSelectAllInGroup(groupKey) {
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-key="${groupKey}"]`);
|
||||
const allSelected = Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||
|
||||
// If all are selected, deselect all; otherwise select all
|
||||
@@ -264,7 +364,7 @@ export class DuplicatesManager {
|
||||
});
|
||||
|
||||
// Update the button text
|
||||
const button = document.querySelector(`.duplicate-group[data-fingerprint="${fingerprint}"] .btn-select-all`);
|
||||
const button = document.querySelector(`.duplicate-group[data-group-key="${groupKey}"] .btn-select-all`);
|
||||
if (button) {
|
||||
button.textContent = !allSelected ? "Deselect All" : "Select All";
|
||||
}
|
||||
@@ -272,8 +372,8 @@ export class DuplicatesManager {
|
||||
this.updateSelectedCount();
|
||||
}
|
||||
|
||||
selectAllInGroup(fingerprint) {
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-fingerprint="${fingerprint}"]`);
|
||||
selectAllInGroup(groupKey) {
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-key="${groupKey}"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = true;
|
||||
this.selectedForDeletion.add(checkbox.dataset.recipeId);
|
||||
@@ -281,7 +381,7 @@ export class DuplicatesManager {
|
||||
});
|
||||
|
||||
// Update the button text
|
||||
const button = document.querySelector(`.duplicate-group[data-fingerprint="${fingerprint}"] .btn-select-all`);
|
||||
const button = document.querySelector(`.duplicate-group[data-group-key="${groupKey}"] .btn-select-all`);
|
||||
if (button) {
|
||||
button.textContent = "Deselect All";
|
||||
}
|
||||
@@ -289,12 +389,12 @@ export class DuplicatesManager {
|
||||
this.updateSelectedCount();
|
||||
}
|
||||
|
||||
selectLatestInGroup(fingerprint) {
|
||||
selectLatestInGroup(groupKey) {
|
||||
// Find all checkboxes in this group
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-fingerprint="${fingerprint}"]`);
|
||||
const checkboxes = document.querySelectorAll(`.selector-checkbox[data-group-key="${groupKey}"]`);
|
||||
|
||||
// Get all the recipes in this group
|
||||
const group = this.duplicateGroups.find(g => g.fingerprint === fingerprint);
|
||||
const group = this.duplicateGroups.find(g => g.key === groupKey);
|
||||
if (!group) return;
|
||||
|
||||
// Sort recipes by date (newest first)
|
||||
@@ -328,7 +428,7 @@ export class DuplicatesManager {
|
||||
selectLatestDuplicates() {
|
||||
// For each duplicate group, select all but the latest recipe
|
||||
this.duplicateGroups.forEach(group => {
|
||||
this.selectLatestInGroup(group.fingerprint);
|
||||
this.selectLatestInGroup(group.key);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user