Switched to danbooru.csv and improved tag parsing (closes #11)
This commit does a few things: - Replaces autocomplete.txt with danbooru.csv, which contains aliases and categories - Categries have been added to the autocompletion, changing the color of the tag and adding `(category)` after the tag. - Aliases have been added to the autcompletion, automatically redirecting to their corresponding tag.
This commit is contained in:
-146179
File diff suppressed because it is too large
Load Diff
+140782
File diff suppressed because it is too large
Load Diff
+87
-19
@@ -35,37 +35,105 @@ export async function loadTags() {
|
||||
|
||||
export async function loadAutocompleteTags() {
|
||||
try {
|
||||
const response = await fetch('autocomplete.txt');
|
||||
const response = await fetch('danbooru.csv');
|
||||
if (!response.ok) {
|
||||
console.log('Autocomplete file not found');
|
||||
console.log('Danbooru CSV not found');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const text = await response.text();
|
||||
const lines = text.split('\n');
|
||||
|
||||
const tags = lines
|
||||
.map(line => {
|
||||
const parts = line.trim().split(',');
|
||||
if (parts.length >= 2) {
|
||||
return {
|
||||
tag: parts[0].trim(),
|
||||
count: parseInt(parts[1]) || 0
|
||||
};
|
||||
|
||||
const tags = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// Parse CSV line handling quotes
|
||||
const parts = parseCsvLine(line);
|
||||
if (parts.length < 3) continue;
|
||||
|
||||
const tag = parts[0].trim();
|
||||
const category = parseInt(parts[1]) || 0;
|
||||
const count = parseInt(parts[2]) || 0;
|
||||
const aliasField = parts[3] || '';
|
||||
|
||||
// Skip category 2 (unused)
|
||||
if (category === 2) continue;
|
||||
|
||||
// Add main tag
|
||||
tags.push({
|
||||
tag: tag,
|
||||
category: category,
|
||||
count: count,
|
||||
isAlias: false
|
||||
});
|
||||
|
||||
// Parse and add aliases
|
||||
if (aliasField) {
|
||||
const aliases = parseAliases(aliasField);
|
||||
for (const alias of aliases) {
|
||||
tags.push({
|
||||
tag: alias,
|
||||
category: category,
|
||||
count: count,
|
||||
isAlias: true,
|
||||
aliasFor: tag
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
console.log(`Loaded ${tags.length} autocomplete tags`);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by count (descending)
|
||||
tags.sort((a, b) => b.count - a.count);
|
||||
|
||||
console.log(`Loaded ${tags.length} tags from Danbooru CSV`);
|
||||
return tags;
|
||||
} catch (error) {
|
||||
console.error('Error loading autocomplete tags:', error);
|
||||
console.error('Error loading Danbooru tags:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseCsvLine(line) {
|
||||
const parts = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
function parseAliases(aliasField) {
|
||||
if (!aliasField) return [];
|
||||
|
||||
// Remove quotes and leading/trailing slashes
|
||||
let cleaned = aliasField.replace(/^["\/]+|["\/]+$/g, '');
|
||||
|
||||
// Split by comma
|
||||
const aliases = cleaned.split(',')
|
||||
.map(a => a.trim())
|
||||
.filter(a => a.length > 0);
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
export async function saveCharacters(characters) {
|
||||
const response = await fetch('/character_editor', {
|
||||
method: 'POST',
|
||||
|
||||
+68
-27
@@ -47,27 +47,26 @@ function detectContext(input) {
|
||||
|
||||
function showAutocomplete(input, context) {
|
||||
const { type, searchTerm, start } = context;
|
||||
|
||||
// For LoRA and embedding, show immediately after typing the prefix
|
||||
|
||||
// For LoRA and embedding, show immediately after typing prefix
|
||||
// For tags, require at least 2 characters
|
||||
if (type === 'tag' && (!searchTerm || searchTerm.length < 2)) {
|
||||
hideAutocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// For LoRA/embedding, allow showing with 0 characters
|
||||
// (right after typing <lora: or embedding:)
|
||||
if ((type === 'lora' || type === 'embedding') &&
|
||||
searchTerm === undefined) {
|
||||
searchTerm === undefined) {
|
||||
hideAutocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
autocompleteState.contextType = type;
|
||||
let filtered = [];
|
||||
|
||||
|
||||
console.log(`Searching ${type}:`, searchTerm);
|
||||
|
||||
|
||||
// Filter based on context type
|
||||
if (type === 'lora') {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
@@ -98,7 +97,7 @@ function showAutocomplete(input, context) {
|
||||
}));
|
||||
console.log('Filtered embeddings:', filtered.length);
|
||||
} else {
|
||||
// Tag search
|
||||
// Tag search with category and alias support
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
filtered = autocompleteState.tags
|
||||
.filter(item =>
|
||||
@@ -107,49 +106,81 @@ function showAutocomplete(input, context) {
|
||||
.slice(0, 10)
|
||||
.map(item => ({
|
||||
display: item.tag,
|
||||
value: item.tag,
|
||||
value: item.isAlias ? item.aliasFor : item.tag,
|
||||
count: item.count,
|
||||
category: item.category,
|
||||
isAlias: item.isAlias,
|
||||
aliasFor: item.aliasFor,
|
||||
type: 'tag'
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
if (filtered.length === 0) {
|
||||
hideAutocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
autocompleteState.filteredTags = filtered;
|
||||
autocompleteState.selectedIndex = 0;
|
||||
autocompleteState.activeElement = input;
|
||||
autocompleteState.currentWord = searchTerm;
|
||||
autocompleteState.wordStart = start;
|
||||
|
||||
|
||||
const dropdown = document.getElementById('autocompleteDropdown');
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
|
||||
filtered.forEach((item, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'autocomplete-item';
|
||||
if (index === 0) {
|
||||
div.classList.add('selected');
|
||||
}
|
||||
|
||||
// Different display based on type
|
||||
if (item.type === 'tag' && item.count) {
|
||||
div.innerHTML = `
|
||||
<span class="autocomplete-tag">${item.display}</span>
|
||||
<span class="autocomplete-count">${item.count}</span>
|
||||
`;
|
||||
} else {
|
||||
div.innerHTML = `
|
||||
<span class="autocomplete-tag">${item.display}</span>
|
||||
`;
|
||||
|
||||
// Add category class for tags
|
||||
if (item.type === 'tag' && item.category !== undefined) {
|
||||
div.classList.add(`tag-category-${item.category}`);
|
||||
}
|
||||
|
||||
|
||||
// Different display based on type
|
||||
if (item.type === 'tag') {
|
||||
if (item.isAlias) {
|
||||
// Alias format: alias -> tag (category) count
|
||||
const categoryLabel = getCategoryLabel(item.category);
|
||||
div.innerHTML = `
|
||||
<span class="autocomplete-tag">
|
||||
<span class="alias-name">${item.display}</span>
|
||||
<span class="alias-arrow"> → </span>
|
||||
<span class="alias-target">${item.value}</span>
|
||||
${categoryLabel ?
|
||||
`<span class="category-label">${categoryLabel}
|
||||
</span>` : ''}
|
||||
</span>
|
||||
<span class="autocomplete-count">${item.count}</span>
|
||||
`;
|
||||
} else {
|
||||
// Regular tag format: tag (category) count
|
||||
const categoryLabel = getCategoryLabel(item.category);
|
||||
div.innerHTML = `
|
||||
<span class="autocomplete-tag">
|
||||
${item.display}
|
||||
${categoryLabel ?
|
||||
`<span class="category-label">${categoryLabel}
|
||||
</span>` : ''}
|
||||
</span>
|
||||
<span class="autocomplete-count">${item.count}</span>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// LoRA/embedding format
|
||||
div.innerHTML = `
|
||||
<span class="autocomplete-tag">${item.display}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
div.onclick = () => selectAutocomplete(index);
|
||||
dropdown.appendChild(div);
|
||||
});
|
||||
|
||||
|
||||
const rect = input.getBoundingClientRect();
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.top = (rect.bottom + 5) + 'px';
|
||||
@@ -157,6 +188,16 @@ function showAutocomplete(input, context) {
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
function getCategoryLabel(category) {
|
||||
const labels = {
|
||||
1: '(artist)',
|
||||
3: '(copyright)',
|
||||
4: '(character)',
|
||||
5: '(meta)'
|
||||
};
|
||||
return labels[category] || '';
|
||||
}
|
||||
|
||||
export function hideAutocomplete() {
|
||||
const dropdown = document.getElementById('autocompleteDropdown');
|
||||
dropdown.style.display = 'none';
|
||||
|
||||
@@ -683,6 +683,9 @@ textarea {
|
||||
|
||||
.autocomplete-tag {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.autocomplete-count {
|
||||
@@ -691,6 +694,43 @@ textarea {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Category-specific colors */
|
||||
.tag-category-1 .autocomplete-tag {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.tag-category-3 .autocomplete-tag {
|
||||
color: #b794f4;
|
||||
}
|
||||
|
||||
.tag-category-4 .autocomplete-tag {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.tag-category-5 .autocomplete-tag {
|
||||
color: #f6e05e;
|
||||
}
|
||||
|
||||
/* Alias display styles */
|
||||
.alias-name {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.alias-arrow {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alias-target {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.autocomplete-dropdown::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user