Add extra prompts, endless generation, random character default, and small fixes
- Add extra positive/negative prompt textareas to all 9 detail pages with session persistence - Add Endless generation button to all detail pages (continuous preview generation until stopped) - Default character selector to "Random Character" on all secondary detail pages - Fix queue clear endpoint (remove spurious auth check) - Refactor app.py into routes/ and services/ modules - Update CLAUDE.md with new architecture documentation - Various data file updates and cleanup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
<span class="status-dot status-checking"></span>
|
||||
<span class="status-label d-none d-xl-inline">ComfyUI</span>
|
||||
</span>
|
||||
<span id="status-mcp" class="service-status" title="MCP" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Danbooru MCP: checking…">
|
||||
<span id="status-mcp" class="service-status" title="MCP" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="MCP: checking…">
|
||||
<span class="status-dot status-checking"></span>
|
||||
<span class="status-label d-none d-xl-inline">MCP</span>
|
||||
</span>
|
||||
@@ -111,12 +111,41 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<small class="text-muted me-auto">Jobs are processed sequentially. Close this window to continue browsing.</small>
|
||||
<button type="button" id="queue-clear-btn" class="btn btn-warning btn-sm" onclick="queueClearAll()">
|
||||
🗑️ Clear Queue
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Navigation Modal -->
|
||||
<div class="modal fade" id="galleryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-transparent border-0">
|
||||
<div class="modal-body p-0 text-center position-relative">
|
||||
<!-- Navigation Arrows -->
|
||||
<button type="button" id="gallery-prev" aria-label="Previous image" class="btn btn-dark btn-lg position-absolute start-0 top-50 translate-middle-y ms-2 opacity-75 hover-opacity-100"
|
||||
style="z-index: 1050; border-radius: 50%; width: 50px; height: 50px; display: flex; align-items: center; justify-content: center;">
|
||||
‹
|
||||
</button>
|
||||
<button type="button" id="gallery-next" aria-label="Next image" class="btn btn-dark btn-lg position-absolute end-0 top-50 translate-middle-y me-2 opacity-75 hover-opacity-100"
|
||||
style="z-index: 1050; border-radius: 50%; width: 50px; height: 50px; display: flex; align-items: center; justify-content: center;">
|
||||
›
|
||||
</button>
|
||||
<!-- Image Counter -->
|
||||
<div id="gallery-counter" class="position-absolute bottom-0 start-50 translate-middle-x mb-3 px-3 py-1 bg-dark rounded text-white opacity-75"
|
||||
style="z-index: 1050; font-size: 0.9rem;">
|
||||
1 / 1
|
||||
</div>
|
||||
<!-- Main Image -->
|
||||
<img id="galleryImage" src="" alt="Gallery Image" class="img-fluid" style="max-height: 90vh; cursor: default;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -345,11 +374,104 @@
|
||||
// ---- Service status indicators ----
|
||||
(function () {
|
||||
const services = [
|
||||
{ id: 'status-comfyui', url: '/api/status/comfyui', label: 'ComfyUI' },
|
||||
{ id: 'status-mcp', url: '/api/status/mcp', label: 'Danbooru MCP' },
|
||||
{ id: 'status-llm', url: '/api/status/llm', label: 'LLM' },
|
||||
{ id: 'status-comfyui', url: '/api/status/comfyui', label: 'ComfyUI' },
|
||||
{ id: 'status-llm', url: '/api/status/llm', label: 'LLM' },
|
||||
];
|
||||
|
||||
// MCP services (checked separately for combined indicator)
|
||||
const mcpServices = [
|
||||
{ url: '/api/status/mcp', label: 'Danbooru MCP', key: 'danbooru' },
|
||||
{ url: '/api/status/character-mcp', label: 'Character MCP', key: 'character' },
|
||||
];
|
||||
|
||||
// Global service status tracker
|
||||
window.serviceStatus = {
|
||||
comfyui: false,
|
||||
mcp: false,
|
||||
characterMcp: false,
|
||||
llm: false,
|
||||
mcpStatuses: {
|
||||
danbooru: false,
|
||||
character: false
|
||||
}
|
||||
};
|
||||
|
||||
function updateButtons() {
|
||||
// Image generation buttons require ComfyUI
|
||||
const imageGenButtons = document.querySelectorAll('.btn-image-gen, [data-requires="comfyui"], #generate-btn, button[form="generate-form"], #batch-generate-btn, #regenerate-all-btn, #generate-all-btn');
|
||||
imageGenButtons.forEach(btn => {
|
||||
if (!window.serviceStatus.comfyui) {
|
||||
btn.disabled = true;
|
||||
btn.title = btn.title || 'ComfyUI is not available';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// JSON generation buttons require Danbooru MCP or LLM
|
||||
const jsonGenButtons = document.querySelectorAll('.btn-json-gen, [data-requires="mcp-llm"], button[data-bs-target="#jsonEditorModal"], #json-save-btn');
|
||||
jsonGenButtons.forEach(btn => {
|
||||
if (!window.serviceStatus.mcp && !window.serviceStatus.llm) {
|
||||
btn.disabled = true;
|
||||
btn.title = btn.title || 'Danbooru MCP or LLM is required';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Character generation buttons require Character MCP or LLM
|
||||
const charGenButtons = document.querySelectorAll('.btn-char-gen, [data-requires="char-mcp-llm"], #submit-btn, button[form*="create"]');
|
||||
charGenButtons.forEach(btn => {
|
||||
if (!window.serviceStatus.characterMcp && !window.serviceStatus.llm) {
|
||||
btn.disabled = true;
|
||||
btn.title = btn.title || 'Character MCP or LLM is required';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateMcpStatus() {
|
||||
const el = document.getElementById('status-mcp');
|
||||
if (!el) return;
|
||||
|
||||
const statuses = window.serviceStatus.mcpStatuses;
|
||||
const allOnline = statuses.danbooru && statuses.character;
|
||||
const allOffline = !statuses.danbooru && !statuses.character;
|
||||
const someOffline = !allOnline && !allOffline;
|
||||
|
||||
// Update global status flags
|
||||
window.serviceStatus.mcp = statuses.danbooru;
|
||||
window.serviceStatus.characterMcp = statuses.character;
|
||||
|
||||
const dot = el.querySelector('.status-dot');
|
||||
|
||||
// Set status class: red (all offline), yellow (some offline), green (all online)
|
||||
if (allOffline) {
|
||||
dot.className = 'status-dot status-error';
|
||||
} else if (someOffline) {
|
||||
dot.className = 'status-dot status-warning';
|
||||
} else {
|
||||
dot.className = 'status-dot status-ok';
|
||||
}
|
||||
|
||||
// Build detailed tooltip
|
||||
const tooltipLines = [
|
||||
'MCP Services:',
|
||||
`Danbooru MCP: ${statuses.danbooru ? 'online' : 'offline'}`,
|
||||
`Character MCP: ${statuses.character ? 'online' : 'offline'}`
|
||||
];
|
||||
const tooltipText = tooltipLines.join('\n');
|
||||
|
||||
el.setAttribute('data-bs-title', tooltipText);
|
||||
el.setAttribute('title', tooltipText);
|
||||
const tip = bootstrap.Tooltip.getInstance(el);
|
||||
if (tip) tip.setContent({ '.tooltip-inner': tooltipText });
|
||||
|
||||
// Update buttons based on new status
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function setStatus(id, label, ok) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
@@ -357,13 +479,19 @@
|
||||
dot.className = 'status-dot ' + (ok ? 'status-ok' : 'status-error');
|
||||
if (id === 'status-comfyui' && window._updateComfyTooltip) {
|
||||
window._updateComfyTooltip();
|
||||
return;
|
||||
}
|
||||
const tooltipText = label + ': ' + (ok ? 'online' : 'offline');
|
||||
el.setAttribute('data-bs-title', tooltipText);
|
||||
el.setAttribute('title', tooltipText);
|
||||
const tip = bootstrap.Tooltip.getInstance(el);
|
||||
if (tip) tip.setContent({ '.tooltip-inner': tooltipText });
|
||||
|
||||
// Update global status
|
||||
if (id === 'status-comfyui') window.serviceStatus.comfyui = ok;
|
||||
if (id === 'status-llm') window.serviceStatus.llm = ok;
|
||||
|
||||
// Update buttons based on new status
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
async function pollService(svc) {
|
||||
@@ -376,8 +504,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
function pollAll() {
|
||||
services.forEach(pollService);
|
||||
async function pollMcpService(svc) {
|
||||
try {
|
||||
const r = await fetch(svc.url, { cache: 'no-store' });
|
||||
const data = await r.json();
|
||||
window.serviceStatus.mcpStatuses[svc.key] = data.status === 'ok';
|
||||
} catch {
|
||||
window.serviceStatus.mcpStatuses[svc.key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollAll() {
|
||||
// Poll regular services
|
||||
await Promise.all(services.map(pollService));
|
||||
|
||||
// Poll MCP services
|
||||
await Promise.all(mcpServices.map(pollMcpService));
|
||||
|
||||
// Update combined MCP status
|
||||
updateMcpStatus();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -405,21 +550,48 @@
|
||||
|
||||
function renderQueue(jobs) {
|
||||
const activeJobs = jobs.filter(j => !['done', 'failed', 'removed'].includes(j.status));
|
||||
const pendingJobs = jobs.filter(j => j.status === 'pending');
|
||||
const processingJob = jobs.find(j => j.status === 'processing');
|
||||
const count = activeJobs.length;
|
||||
const queueBtn = document.getElementById('queue-btn');
|
||||
|
||||
// Update badge
|
||||
if (count > 0) {
|
||||
badge.textContent = count;
|
||||
badge.classList.remove('d-none');
|
||||
document.getElementById('queue-btn').classList.add('queue-btn-active');
|
||||
queueBtn.classList.add('queue-btn-active');
|
||||
} else {
|
||||
badge.classList.add('d-none');
|
||||
document.getElementById('queue-btn').classList.remove('queue-btn-active');
|
||||
queueBtn.classList.remove('queue-btn-active');
|
||||
}
|
||||
|
||||
// Update generating animation and tooltip
|
||||
if (processingJob) {
|
||||
queueBtn.classList.add('queue-btn-generating');
|
||||
queueBtn.title = `Generating: ${processingJob.label}`;
|
||||
} else if (pendingJobs.length > 0) {
|
||||
queueBtn.classList.remove('queue-btn-generating');
|
||||
queueBtn.title = `${pendingJobs.length} job(s) queued`;
|
||||
} else {
|
||||
queueBtn.classList.remove('queue-btn-generating');
|
||||
queueBtn.title = 'Generation Queue';
|
||||
}
|
||||
|
||||
// Update modal count
|
||||
if (modalCount) modalCount.textContent = jobs.length;
|
||||
|
||||
// Update Clear Queue button state
|
||||
const clearBtn = document.getElementById('queue-clear-btn');
|
||||
if (clearBtn) {
|
||||
if (pendingJobs.length > 0) {
|
||||
clearBtn.disabled = false;
|
||||
clearBtn.title = `Clear ${pendingJobs.length} pending job(s)`;
|
||||
} else {
|
||||
clearBtn.disabled = true;
|
||||
clearBtn.title = 'No pending jobs to clear';
|
||||
}
|
||||
}
|
||||
|
||||
// Render job list
|
||||
if (!jobList) return;
|
||||
if (jobs.length === 0) {
|
||||
@@ -515,6 +687,27 @@
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function queueClearAll() {
|
||||
if (!confirm('Clear all pending jobs from the queue? The current generation will continue.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/queue/clear', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.removed_count > 0) {
|
||||
alert(`Cleared ${data.removed_count} pending job(s) from the queue.`);
|
||||
} else {
|
||||
alert('No pending jobs to clear.');
|
||||
}
|
||||
fetchQueue();
|
||||
} catch (e) {
|
||||
alert('Error clearing queue: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Make queueClearAll globally accessible
|
||||
window.queueClearAll = queueClearAll;
|
||||
|
||||
// Poll queue every 2 seconds
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchQueue();
|
||||
@@ -528,6 +721,240 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// ---- Gallery Navigation System ----
|
||||
(function() {
|
||||
let galleryImages = [];
|
||||
let currentIndex = 0;
|
||||
let galleryModal = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const modalEl = document.getElementById('galleryModal');
|
||||
if (modalEl) {
|
||||
galleryModal = new bootstrap.Modal(modalEl);
|
||||
|
||||
// Previous button
|
||||
document.getElementById('gallery-prev').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
navigateGallery(-1);
|
||||
});
|
||||
|
||||
// Next button
|
||||
document.getElementById('gallery-next').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
navigateGallery(1);
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
modalEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
navigateGallery(-1);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
navigateGallery(1);
|
||||
} else if (e.key === 'Escape') {
|
||||
galleryModal.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Click on image to go next (optional convenience)
|
||||
document.getElementById('galleryImage').addEventListener('click', () => {
|
||||
navigateGallery(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function navigateGallery(direction) {
|
||||
if (galleryImages.length === 0) return;
|
||||
|
||||
currentIndex += direction;
|
||||
|
||||
// Wrap around
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = galleryImages.length - 1;
|
||||
} else if (currentIndex >= galleryImages.length) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
updateGalleryImage();
|
||||
}
|
||||
|
||||
function updateGalleryImage() {
|
||||
const img = document.getElementById('galleryImage');
|
||||
const counter = document.getElementById('gallery-counter');
|
||||
|
||||
if (galleryImages[currentIndex]) {
|
||||
img.src = galleryImages[currentIndex];
|
||||
counter.textContent = `${currentIndex + 1} / ${galleryImages.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Global function to open gallery
|
||||
window.openGallery = function(images, startIndex = 0) {
|
||||
galleryImages = images || [];
|
||||
currentIndex = startIndex;
|
||||
|
||||
if (galleryImages.length === 0) return;
|
||||
|
||||
updateGalleryImage();
|
||||
galleryModal.show();
|
||||
};
|
||||
|
||||
// Global function to register gallery images from a container
|
||||
window.registerGallery = function(containerSelector, imageSelector) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container) return [];
|
||||
|
||||
const images = [];
|
||||
container.querySelectorAll(imageSelector).forEach((img, index) => {
|
||||
const src = img.src || img.dataset.src;
|
||||
if (src) {
|
||||
images.push(src);
|
||||
img.style.cursor = 'pointer';
|
||||
img.addEventListener('click', () => {
|
||||
openGallery(images, index);
|
||||
});
|
||||
}
|
||||
});
|
||||
return images;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Seed input: clear button and auto-populate from generation result
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const seedInput = document.getElementById('seed-input');
|
||||
const seedClearBtn = document.getElementById('seed-clear-btn');
|
||||
if (seedClearBtn && seedInput) {
|
||||
seedClearBtn.addEventListener('click', () => { seedInput.value = ''; });
|
||||
}
|
||||
});
|
||||
// Global helper: update seed input after a generation job completes
|
||||
function updateSeedFromResult(jobResult) {
|
||||
const seedInput = document.getElementById('seed-input');
|
||||
if (seedInput && jobResult?.seed != null) {
|
||||
seedInput.value = jobResult.seed;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
// Endless generation mode
|
||||
(function() {
|
||||
let _endless = false;
|
||||
let _endlessCount = 0;
|
||||
let _endlessAbort = null;
|
||||
|
||||
window._endlessActive = () => _endless;
|
||||
|
||||
window._endlessStart = function() {
|
||||
const form = document.getElementById('generate-form');
|
||||
const btn = document.getElementById('endless-btn');
|
||||
const stopBtn = document.getElementById('endless-stop-btn');
|
||||
const counter = document.getElementById('endless-counter');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressLabel = document.getElementById('progress-label');
|
||||
if (!form || !btn) return;
|
||||
|
||||
_endless = true;
|
||||
_endlessCount = 0;
|
||||
_endlessAbort = new AbortController();
|
||||
btn.classList.add('d-none');
|
||||
stopBtn.classList.remove('d-none');
|
||||
counter.classList.remove('d-none');
|
||||
counter.textContent = '0 generated';
|
||||
|
||||
// Disable single generate button during endless
|
||||
const genBtn = form.querySelector('button[value="preview"]');
|
||||
if (genBtn) genBtn.disabled = true;
|
||||
|
||||
(async function loop() {
|
||||
while (_endless) {
|
||||
// Clear seed for random each time
|
||||
const seedInput = document.getElementById('seed-input');
|
||||
if (seedInput) seedInput.value = '';
|
||||
|
||||
const formData = new FormData(form);
|
||||
formData.set('action', 'preview');
|
||||
|
||||
// Show progress
|
||||
if (progressContainer) {
|
||||
progressContainer.classList.remove('d-none');
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '';
|
||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
progressLabel.textContent = `Endless #${_endlessCount + 1} — Queuing…`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(form.getAttribute('action'), {
|
||||
method: 'POST', body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
signal: _endlessAbort.signal
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) { console.error('Endless error:', data.error); break; }
|
||||
|
||||
if (progressLabel) progressLabel.textContent = `Endless #${_endlessCount + 1} — Generating…`;
|
||||
|
||||
// Poll for completion
|
||||
const jobResult = await new Promise((resolve, reject) => {
|
||||
const poll = setInterval(async () => {
|
||||
if (!_endless) { clearInterval(poll); reject(new Error('stopped')); return; }
|
||||
try {
|
||||
const resp = await fetch(`/api/queue/${data.job_id}/status`);
|
||||
const status = await resp.json();
|
||||
if (status.status === 'done') { clearInterval(poll); resolve(status); }
|
||||
else if (status.status === 'failed' || status.status === 'removed') {
|
||||
clearInterval(poll); reject(new Error(status.error || 'Job failed'));
|
||||
} else if (status.status === 'processing' && progressLabel) {
|
||||
progressLabel.textContent = `Endless #${_endlessCount + 1} — Generating…`;
|
||||
}
|
||||
} catch (err) { /* keep polling */ }
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
_endlessCount++;
|
||||
counter.textContent = `${_endlessCount} generated`;
|
||||
|
||||
// Update preview via page-specific handler
|
||||
if (jobResult.result?.image_url && window._onEndlessResult) {
|
||||
window._onEndlessResult(jobResult);
|
||||
}
|
||||
updateSeedFromResult(jobResult.result);
|
||||
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError' || err.message === 'stopped') break;
|
||||
console.error('Endless generation error:', err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Cleanup
|
||||
window._endlessStop();
|
||||
})();
|
||||
};
|
||||
|
||||
window._endlessStop = function() {
|
||||
_endless = false;
|
||||
if (_endlessAbort) { _endlessAbort.abort(); _endlessAbort = null; }
|
||||
const btn = document.getElementById('endless-btn');
|
||||
const stopBtn = document.getElementById('endless-stop-btn');
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const form = document.getElementById('generate-form');
|
||||
if (btn) btn.classList.remove('d-none');
|
||||
if (stopBtn) stopBtn.classList.add('d-none');
|
||||
if (progressContainer) progressContainer.classList.add('d-none');
|
||||
if (progressBar) progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||
const genBtn = form?.querySelector('button[value="preview"]');
|
||||
if (genBtn) genBtn.disabled = false;
|
||||
};
|
||||
|
||||
// Stop on page leave
|
||||
window.addEventListener('beforeunload', () => { if (_endless) window._endlessStop(); });
|
||||
})();
|
||||
</script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user