mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
chore(priority-tags): add newline terminator
This commit is contained in:
@@ -4,7 +4,47 @@
|
||||
*/
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
import { PRESET_TAGS } from '../../utils/constants.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { getPriorityTagSuggestions } from '../../utils/priorityTagHelpers.js';
|
||||
|
||||
let priorityTagSuggestions = [];
|
||||
let priorityTagSuggestionsLoaded = false;
|
||||
let priorityTagSuggestionsPromise = null;
|
||||
|
||||
function ensurePriorityTagSuggestions() {
|
||||
if (!priorityTagSuggestionsPromise) {
|
||||
priorityTagSuggestionsPromise = getPriorityTagSuggestions()
|
||||
.then((tags) => {
|
||||
priorityTagSuggestions = tags;
|
||||
priorityTagSuggestionsLoaded = true;
|
||||
return tags;
|
||||
})
|
||||
.catch(() => {
|
||||
priorityTagSuggestions = [];
|
||||
priorityTagSuggestionsLoaded = true;
|
||||
return priorityTagSuggestions;
|
||||
})
|
||||
.finally(() => {
|
||||
priorityTagSuggestionsPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return priorityTagSuggestionsLoaded && !priorityTagSuggestionsPromise
|
||||
? Promise.resolve(priorityTagSuggestions)
|
||||
: priorityTagSuggestionsPromise;
|
||||
}
|
||||
|
||||
ensurePriorityTagSuggestions();
|
||||
|
||||
window.addEventListener('lm:priority-tags-updated', () => {
|
||||
priorityTagSuggestionsLoaded = false;
|
||||
ensurePriorityTagSuggestions().then(() => {
|
||||
document.querySelectorAll('.metadata-edit-container .metadata-suggestions-container').forEach((container) => {
|
||||
renderPriorityTagSuggestions(container, getCurrentEditTags());
|
||||
});
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
});
|
||||
|
||||
// Create a named function so we can remove it later
|
||||
let saveTagsHandler = null;
|
||||
@@ -260,7 +300,7 @@ function createTagEditUI(currentTags, editBtnHTML = '') {
|
||||
function createSuggestionsDropdown(existingTags = []) {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'metadata-suggestions-dropdown';
|
||||
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'metadata-suggestions-header';
|
||||
@@ -273,11 +313,33 @@ function createSuggestionsDropdown(existingTags = []) {
|
||||
// Create tag container
|
||||
const container = document.createElement('div');
|
||||
container.className = 'metadata-suggestions-container';
|
||||
|
||||
// Add each preset tag as a suggestion
|
||||
PRESET_TAGS.forEach(tag => {
|
||||
if (priorityTagSuggestionsLoaded && !priorityTagSuggestionsPromise) {
|
||||
renderPriorityTagSuggestions(container, existingTags);
|
||||
} else {
|
||||
container.innerHTML = `<div class="metadata-suggestions-loading">${translate('settings.priorityTags.loadingSuggestions', 'Loading suggestions…')}</div>`;
|
||||
ensurePriorityTagSuggestions().then(() => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
renderPriorityTagSuggestions(container, getCurrentEditTags());
|
||||
updateSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
if (container.isConnected) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
function renderPriorityTagSuggestions(container, existingTags = []) {
|
||||
container.innerHTML = '';
|
||||
|
||||
priorityTagSuggestions.forEach((tag) => {
|
||||
const isAdded = existingTags.includes(tag);
|
||||
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = tag;
|
||||
@@ -285,28 +347,21 @@ function createSuggestionsDropdown(existingTags = []) {
|
||||
<span class="metadata-suggestion-text">${tag}</span>
|
||||
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||
`;
|
||||
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
addNewTag(tag);
|
||||
|
||||
// Also populate the input field for potential editing
|
||||
|
||||
const input = document.querySelector('.metadata-input');
|
||||
if (input) input.value = tag;
|
||||
|
||||
// Focus on the input
|
||||
if (input) input.focus();
|
||||
|
||||
// Update dropdown without removing it
|
||||
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,10 +461,9 @@ function addNewTag(tag) {
|
||||
function updateSuggestionsDropdown() {
|
||||
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
|
||||
// Get all current tags
|
||||
const currentTags = document.querySelectorAll('.metadata-item');
|
||||
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
const existingTags = getCurrentEditTags();
|
||||
|
||||
// Update status of each item in dropdown
|
||||
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
||||
@@ -456,6 +510,11 @@ function updateSuggestionsDropdown() {
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentEditTags() {
|
||||
const currentTags = document.querySelectorAll('.metadata-item');
|
||||
return Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original tags when canceling edit
|
||||
* @param {HTMLElement} section - The tags section
|
||||
|
||||
@@ -4,7 +4,8 @@ import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||
import { eventManager } from '../utils/EventManager.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
|
||||
@@ -59,6 +60,22 @@ export class BulkManager {
|
||||
setContentRating: true
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('lm:priority-tags-updated', () => {
|
||||
const container = document.querySelector('#bulkAddTagsModal .metadata-suggestions-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
getPriorityTagSuggestions().then((tags) => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
this.renderBulkSuggestionItems(container, tags);
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
// Ignore refresh failures; UI will retry on next open
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -565,7 +582,7 @@ export class BulkManager {
|
||||
// Create suggestions dropdown
|
||||
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||
if (tagForm) {
|
||||
const suggestionsDropdown = this.createBulkSuggestionsDropdown(PRESET_TAGS);
|
||||
const suggestionsDropdown = this.createBulkSuggestionsDropdown();
|
||||
tagForm.appendChild(suggestionsDropdown);
|
||||
}
|
||||
|
||||
@@ -586,10 +603,10 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
createBulkSuggestionsDropdown(presetTags) {
|
||||
createBulkSuggestionsDropdown() {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'metadata-suggestions-dropdown';
|
||||
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'metadata-suggestions-header';
|
||||
header.innerHTML = `
|
||||
@@ -597,15 +614,34 @@ export class BulkManager {
|
||||
<small>Click to add</small>
|
||||
`;
|
||||
dropdown.appendChild(header);
|
||||
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'metadata-suggestions-container';
|
||||
|
||||
presetTags.forEach(tag => {
|
||||
// Check if tag is already added
|
||||
container.innerHTML = `<div class="metadata-suggestions-loading">${translate('settings.priorityTags.loadingSuggestions', 'Loading suggestions…')}</div>`;
|
||||
|
||||
getPriorityTagSuggestions().then((tags) => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
this.renderBulkSuggestionItems(container, tags);
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
if (container.isConnected) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
renderBulkSuggestionItems(container, tags) {
|
||||
container.innerHTML = '';
|
||||
|
||||
tags.forEach(tag => {
|
||||
const existingTags = this.getBulkExistingTags();
|
||||
const isAdded = existingTags.includes(tag);
|
||||
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = tag;
|
||||
@@ -613,7 +649,7 @@ export class BulkManager {
|
||||
<span class="metadata-suggestion-text">${tag}</span>
|
||||
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||
`;
|
||||
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
this.addBulkTag(tag);
|
||||
@@ -622,16 +658,12 @@ export class BulkManager {
|
||||
input.value = tag;
|
||||
input.focus();
|
||||
}
|
||||
// Update dropdown to show added indicator
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
addBulkTag(tag) {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { modalManager } from './ModalManager.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state, createDefaultSettings } from '../state/index.js';
|
||||
import { resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/constants.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { i18n } from '../i18n/index.js';
|
||||
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
|
||||
import { validatePriorityTagString, getPriorityTagSuggestionsMap, invalidatePriorityTagSuggestionsCache } from '../utils/priorityTagHelpers.js';
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
@@ -111,6 +112,17 @@ export class SettingsManager {
|
||||
|
||||
merged.download_path_templates = { ...DEFAULT_PATH_TEMPLATES, ...templates };
|
||||
|
||||
const priorityTags = backendSettings?.priority_tags;
|
||||
const normalizedPriority = { ...DEFAULT_PRIORITY_TAG_CONFIG };
|
||||
if (priorityTags && typeof priorityTags === 'object' && !Array.isArray(priorityTags)) {
|
||||
Object.entries(priorityTags).forEach(([modelType, configValue]) => {
|
||||
if (typeof configValue === 'string') {
|
||||
normalizedPriority[modelType] = configValue.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
merged.priority_tags = normalizedPriority;
|
||||
|
||||
Object.keys(merged).forEach(key => this.backendSettingKeys.add(key));
|
||||
|
||||
return merged;
|
||||
@@ -201,14 +213,14 @@ export class SettingsManager {
|
||||
settingsManager.validateTemplate(modelType, template);
|
||||
settingsManager.updateTemplatePreview(modelType, template);
|
||||
});
|
||||
|
||||
|
||||
customInput.addEventListener('blur', (e) => {
|
||||
const template = e.target.value;
|
||||
if (settingsManager.validateTemplate(modelType, template)) {
|
||||
settingsManager.updateTemplate(modelType, template);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
customInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur();
|
||||
@@ -216,7 +228,9 @@ export class SettingsManager {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.setupPriorityTagInputs();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
@@ -291,6 +305,9 @@ export class SettingsManager {
|
||||
// Load download path templates
|
||||
this.loadDownloadPathTemplates();
|
||||
|
||||
// Load priority tag settings
|
||||
this.loadPriorityTagSettings();
|
||||
|
||||
// Set include trigger words setting
|
||||
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
||||
if (includeTriggerWordsCheckbox) {
|
||||
@@ -325,6 +342,131 @@ export class SettingsManager {
|
||||
this.loadProxySettings();
|
||||
}
|
||||
|
||||
setupPriorityTagInputs() {
|
||||
['lora', 'checkpoint', 'embedding'].forEach((modelType) => {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', () => this.handlePriorityTagInput(modelType));
|
||||
textarea.addEventListener('blur', () => this.handlePriorityTagBlur(modelType));
|
||||
});
|
||||
}
|
||||
|
||||
loadPriorityTagSettings() {
|
||||
const priorityConfig = state.global.settings.priority_tags || {};
|
||||
['lora', 'checkpoint', 'embedding'].forEach((modelType) => {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedValue = priorityConfig[modelType] ?? DEFAULT_PRIORITY_TAG_CONFIG[modelType] ?? '';
|
||||
textarea.value = storedValue;
|
||||
this.displayPriorityTagValidation(modelType, true, []);
|
||||
});
|
||||
}
|
||||
|
||||
handlePriorityTagInput(modelType) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validatePriorityTagString(textarea.value);
|
||||
this.displayPriorityTagValidation(modelType, validation.valid, validation.errors);
|
||||
}
|
||||
|
||||
async handlePriorityTagBlur(modelType) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validatePriorityTagString(textarea.value);
|
||||
if (!validation.valid) {
|
||||
this.displayPriorityTagValidation(modelType, false, validation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = validation.formatted;
|
||||
const currentValue = state.global.settings.priority_tags?.[modelType] || '';
|
||||
this.displayPriorityTagValidation(modelType, true, []);
|
||||
|
||||
if (sanitized === currentValue) {
|
||||
textarea.value = sanitized;
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...state.global.settings.priority_tags,
|
||||
[modelType]: sanitized,
|
||||
};
|
||||
|
||||
try {
|
||||
textarea.value = sanitized;
|
||||
await this.saveSetting('priority_tags', updatedConfig);
|
||||
showToast('settings.priorityTags.saveSuccess', {}, 'success');
|
||||
await this.refreshPriorityTagSuggestions();
|
||||
} catch (error) {
|
||||
console.error('Failed to save priority tag configuration:', error);
|
||||
showToast('settings.priorityTags.saveError', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
displayPriorityTagValidation(modelType, isValid, errors = []) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
const errorElement = document.getElementById(`${modelType}PriorityTagsError`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValid || errors.length === 0) {
|
||||
textarea.classList.remove('input-error');
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.classList.add('input-error');
|
||||
if (errorElement) {
|
||||
const message = this.getPriorityTagErrorMessage(errors[0]);
|
||||
errorElement.textContent = message;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
getPriorityTagErrorMessage(error) {
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const entryIndex = error.index ?? 0;
|
||||
switch (error.type) {
|
||||
case 'missingClosingParen':
|
||||
return translate('settings.priorityTags.validation.missingClosingParen', { index: entryIndex }, `Entry ${entryIndex} is missing a closing parenthesis.`);
|
||||
case 'missingCanonical':
|
||||
return translate('settings.priorityTags.validation.missingCanonical', { index: entryIndex }, `Entry ${entryIndex} must include a canonical tag.`);
|
||||
case 'duplicateCanonical':
|
||||
return translate('settings.priorityTags.validation.duplicateCanonical', { tag: error.canonical }, `The canonical tag "${error.canonical}" is duplicated.`);
|
||||
default:
|
||||
return translate('settings.priorityTags.validation.unknown', {}, 'Invalid priority tag configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
async refreshPriorityTagSuggestions() {
|
||||
invalidatePriorityTagSuggestionsCache();
|
||||
try {
|
||||
await getPriorityTagSuggestionsMap();
|
||||
window.dispatchEvent(new CustomEvent('lm:priority-tags-updated'));
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh priority tag suggestions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadProxySettings() {
|
||||
// Load proxy enabled setting
|
||||
const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Create the new hierarchical state structure
|
||||
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/constants.js';
|
||||
|
||||
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
civitai_api_key: '',
|
||||
@@ -28,6 +28,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
card_info_display: 'always',
|
||||
include_trigger_words: false,
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
});
|
||||
|
||||
export function createDefaultSettings() {
|
||||
@@ -35,6 +36,7 @@ export function createDefaultSettings() {
|
||||
...DEFAULT_SETTINGS_BASE,
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -194,10 +194,16 @@ export const BASE_MODEL_CATEGORIES = {
|
||||
]
|
||||
};
|
||||
|
||||
// Preset tag suggestions
|
||||
export const PRESET_TAGS = [
|
||||
// Default priority tag entries for fallback suggestions and initial settings
|
||||
export const DEFAULT_PRIORITY_TAG_ENTRIES = [
|
||||
'character', 'concept', 'clothing',
|
||||
'realistic', 'anime', 'toon', 'furry', 'style',
|
||||
'poses', 'background', 'vehicle', 'buildings',
|
||||
'objects', 'animal'
|
||||
'poses', 'background', 'tool', 'vehicle', 'buildings',
|
||||
'objects', 'assets', 'animal', 'action'
|
||||
];
|
||||
|
||||
export const DEFAULT_PRIORITY_TAG_CONFIG = {
|
||||
lora: DEFAULT_PRIORITY_TAG_ENTRIES.join(', '),
|
||||
checkpoint: DEFAULT_PRIORITY_TAG_ENTRIES.join(', '),
|
||||
embedding: DEFAULT_PRIORITY_TAG_ENTRIES.join(', ')
|
||||
};
|
||||
|
||||
223
static/js/utils/priorityTagHelpers.js
Normal file
223
static/js/utils/priorityTagHelpers.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { DEFAULT_PRIORITY_TAG_CONFIG } from './constants.js';
|
||||
|
||||
function splitPriorityEntries(raw = '') {
|
||||
const segments = [];
|
||||
raw.split('\n').forEach(line => {
|
||||
line.split(',').forEach(part => {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed) {
|
||||
segments.push(trimmed);
|
||||
}
|
||||
});
|
||||
});
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function parsePriorityTagString(raw = '') {
|
||||
const entries = [];
|
||||
const rawEntries = splitPriorityEntries(raw);
|
||||
|
||||
rawEntries.forEach((entry) => {
|
||||
const { canonical, aliases } = parsePriorityEntry(entry);
|
||||
if (!canonical) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.push({ canonical, aliases });
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parsePriorityEntry(entry) {
|
||||
let canonical = entry;
|
||||
let aliasSection = '';
|
||||
|
||||
const openIndex = entry.indexOf('(');
|
||||
if (openIndex !== -1) {
|
||||
if (!entry.endsWith(')')) {
|
||||
canonical = entry.replace('(', '').replace(')', '');
|
||||
} else {
|
||||
canonical = entry.slice(0, openIndex).trim();
|
||||
aliasSection = entry.slice(openIndex + 1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
canonical = canonical.trim();
|
||||
if (!canonical) {
|
||||
return { canonical: '', aliases: [] };
|
||||
}
|
||||
|
||||
const aliasList = aliasSection ? aliasSection.split('|').map((alias) => alias.trim()).filter(Boolean) : [];
|
||||
const seen = new Set();
|
||||
const normalizedCanonical = canonical.toLowerCase();
|
||||
const uniqueAliases = [];
|
||||
|
||||
aliasList.forEach((alias) => {
|
||||
const normalized = alias.toLowerCase();
|
||||
if (normalized === normalizedCanonical) {
|
||||
return;
|
||||
}
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
uniqueAliases.push(alias);
|
||||
}
|
||||
});
|
||||
|
||||
return { canonical, aliases: uniqueAliases };
|
||||
}
|
||||
|
||||
export function formatPriorityTagEntries(entries, useNewlines = false) {
|
||||
if (!entries.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const separator = useNewlines ? ',\n' : ', ';
|
||||
return entries.map(({ canonical, aliases }) => {
|
||||
if (aliases && aliases.length) {
|
||||
return `${canonical}(${aliases.join('|')})`;
|
||||
}
|
||||
return canonical;
|
||||
}).join(separator);
|
||||
}
|
||||
|
||||
export function validatePriorityTagString(raw = '') {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { valid: true, errors: [], entries: [], formatted: '' };
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
const entries = [];
|
||||
const rawEntries = splitPriorityEntries(raw);
|
||||
const seenCanonicals = new Set();
|
||||
|
||||
rawEntries.forEach((entry, index) => {
|
||||
const hasOpening = entry.includes('(');
|
||||
const hasClosing = entry.endsWith(')');
|
||||
|
||||
if (hasOpening && !hasClosing) {
|
||||
errors.push({ type: 'missingClosingParen', index: index + 1 });
|
||||
}
|
||||
|
||||
const { canonical, aliases } = parsePriorityEntry(entry);
|
||||
if (!canonical) {
|
||||
errors.push({ type: 'missingCanonical', index: index + 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedCanonical = canonical.toLowerCase();
|
||||
if (seenCanonicals.has(normalizedCanonical)) {
|
||||
errors.push({ type: 'duplicateCanonical', canonical });
|
||||
} else {
|
||||
seenCanonicals.add(normalizedCanonical);
|
||||
}
|
||||
|
||||
entries.push({ canonical, aliases });
|
||||
});
|
||||
|
||||
const formatted = errors.length === 0
|
||||
? formatPriorityTagEntries(entries, raw.includes('\n'))
|
||||
: raw.trim();
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
entries,
|
||||
formatted,
|
||||
};
|
||||
}
|
||||
|
||||
let cachedPriorityTagMap = null;
|
||||
let fetchPromise = null;
|
||||
|
||||
export async function getPriorityTagSuggestionsMap() {
|
||||
if (cachedPriorityTagMap) {
|
||||
return cachedPriorityTagMap;
|
||||
}
|
||||
|
||||
if (!fetchPromise) {
|
||||
fetchPromise = fetch('/api/lm/priority-tags')
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data || data.success === false || typeof data.tags !== 'object') {
|
||||
throw new Error(data?.error || 'Invalid response payload');
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(data.tags).forEach(([modelType, tags]) => {
|
||||
if (!Array.isArray(tags)) {
|
||||
return;
|
||||
}
|
||||
normalized[modelType] = tags.filter(tag => typeof tag === 'string' && tag.trim());
|
||||
});
|
||||
|
||||
const withDefaults = applyDefaultPriorityTagFallback(normalized);
|
||||
cachedPriorityTagMap = withDefaults;
|
||||
return withDefaults;
|
||||
})
|
||||
.catch(() => {
|
||||
const fallback = buildDefaultPriorityTagMap();
|
||||
cachedPriorityTagMap = fallback;
|
||||
return fallback;
|
||||
})
|
||||
.finally(() => {
|
||||
fetchPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
export async function getPriorityTagSuggestions() {
|
||||
const map = await getPriorityTagSuggestionsMap();
|
||||
const unique = new Set();
|
||||
Object.values(map).forEach((tags) => {
|
||||
tags.forEach((tag) => {
|
||||
unique.add(tag);
|
||||
});
|
||||
});
|
||||
return Array.from(unique);
|
||||
}
|
||||
|
||||
function applyDefaultPriorityTagFallback(map) {
|
||||
const result = { ...buildDefaultPriorityTagMap(), ...map };
|
||||
Object.entries(result).forEach(([key, tags]) => {
|
||||
result[key] = dedupeTags(Array.isArray(tags) ? tags : []);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildDefaultPriorityTagMap() {
|
||||
const map = {};
|
||||
Object.entries(DEFAULT_PRIORITY_TAG_CONFIG).forEach(([modelType, configString]) => {
|
||||
const entries = parsePriorityTagString(configString);
|
||||
map[modelType] = entries.map((entry) => entry.canonical);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function dedupeTags(tags) {
|
||||
const seen = new Set();
|
||||
const ordered = [];
|
||||
tags.forEach((tag) => {
|
||||
const normalized = tag.toLowerCase();
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
ordered.push(tag);
|
||||
}
|
||||
});
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export function getDefaultPriorityTagConfig() {
|
||||
return { ...DEFAULT_PRIORITY_TAG_CONFIG };
|
||||
}
|
||||
|
||||
export function invalidatePriorityTagSuggestionsCache() {
|
||||
cachedPriorityTagMap = null;
|
||||
fetchPromise = null;
|
||||
}
|
||||
Reference in New Issue
Block a user