Add danbooru-mcp auto-start, git sync, status API endpoints, navbar status indicators, and LLM format retry
- app.py: add subprocess import; add _ensure_mcp_repo() to clone/pull danbooru-mcp from https://git.liveaodh.com/aodhan/danbooru-mcp into tools/danbooru-mcp/ at startup; add ensure_mcp_server_running() which calls _ensure_mcp_repo() then starts the Docker container if not running; add GET /api/status/comfyui and GET /api/status/mcp health endpoints; fix call_llm() to retry up to 3 times on unexpected response format (KeyError/IndexError), logging the raw response and prompting the LLM to respond with valid JSON before each retry - templates/layout.html: add ComfyUI and MCP status dot indicators to navbar; add polling JS that checks both endpoints on load and every 30s - static/style.css: add .service-status, .status-dot, .status-ok, .status-error, .status-checking styles and status-pulse keyframe animation - .gitignore: add tools/ to exclude the cloned danbooru-mcp repo
This commit is contained in:
@@ -1,49 +1,6 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.gallery-card { position: relative; overflow: hidden; border-radius: 8px; background: #1a1a1a; cursor: pointer; }
|
||||
.gallery-card img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; transition: transform 0.2s; }
|
||||
.gallery-card:hover img { transform: scale(1.04); }
|
||||
.gallery-card .overlay {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.82));
|
||||
padding: 28px 8px 8px; opacity: 0; transition: opacity 0.2s;
|
||||
}
|
||||
.gallery-card:hover .overlay { opacity: 1; }
|
||||
.gallery-card .cat-badge {
|
||||
position: absolute; top: 6px; left: 6px;
|
||||
font-size: 0.65rem; text-transform: uppercase; letter-spacing: .04em;
|
||||
}
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 768px) { .gallery-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
|
||||
@media (min-width: 1200px) { .gallery-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } }
|
||||
|
||||
/* Lightbox */
|
||||
#lightbox { display:none; position:fixed; inset:0; background:rgba(0,0,0,.92); z-index:9999;
|
||||
align-items:center; justify-content:center; flex-direction:column; }
|
||||
#lightbox.active { display:flex; }
|
||||
#lightbox-img-wrap { position:relative; }
|
||||
#lightbox-img { max-width:90vw; max-height:80vh; object-fit:contain; border-radius:6px;
|
||||
box-shadow:0 0 40px rgba(0,0,0,.8); cursor:zoom-in; display:block; }
|
||||
#lightbox-meta { color:#eee; margin-top:10px; text-align:center; font-size:.85rem; }
|
||||
#lightbox-hint { color:rgba(255,255,255,.45); font-size:.75rem; margin-top:3px; }
|
||||
#lightbox-close { position:fixed; top:16px; right:20px; font-size:2rem; color:#fff; cursor:pointer; z-index:10000; line-height:1; }
|
||||
#lightbox-prompt-btn { position:fixed; bottom:20px; right:20px; z-index:10000; }
|
||||
|
||||
/* Prompt modal meta grid */
|
||||
.meta-grid { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; font-size:.85rem; }
|
||||
.meta-grid .meta-label { color:#6c757d; white-space:nowrap; font-weight:600; }
|
||||
.meta-grid .meta-value { font-family:monospace; word-break:break-all; }
|
||||
.lora-chip { display:inline-flex; align-items:center; gap:4px; background:#f0f0f0;
|
||||
border-radius:4px; padding:2px 8px; font-size:.8rem; font-family:monospace; margin:2px; }
|
||||
.lora-chip .lora-strength { color:#6c757d; }
|
||||
</style>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Gallery
|
||||
<span class="text-muted fs-6 fw-normal ms-2">{{ total }} image{{ 's' if total != 1 else '' }}</span>
|
||||
@@ -130,12 +87,13 @@
|
||||
{% if images %}
|
||||
<div class="gallery-grid mb-4">
|
||||
{% set cat_colors = {
|
||||
'characters': 'primary',
|
||||
'actions': 'danger',
|
||||
'outfits': 'success',
|
||||
'scenes': 'info',
|
||||
'styles': 'warning',
|
||||
'detailers': 'secondary',
|
||||
'characters': 'primary',
|
||||
'actions': 'danger',
|
||||
'outfits': 'success',
|
||||
'scenes': 'info',
|
||||
'styles': 'warning',
|
||||
'detailers': 'secondary',
|
||||
'checkpoints': 'dark',
|
||||
} %}
|
||||
{% for img in images %}
|
||||
<div class="gallery-card"
|
||||
@@ -162,11 +120,20 @@
|
||||
<a href="{{ url_for('detail', slug=img.slug) }}"
|
||||
class="btn btn-sm btn-outline-light py-0 px-2"
|
||||
onclick="event.stopPropagation()">Open</a>
|
||||
{% elif img.category == 'checkpoints' %}
|
||||
<a href="{{ url_for('checkpoint_detail', slug=img.slug) }}"
|
||||
class="btn btn-sm btn-outline-light py-0 px-2"
|
||||
onclick="event.stopPropagation()">Open</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('generator') }}?{{ img.category[:-1] }}={{ img.slug }}"
|
||||
class="btn btn-sm btn-outline-light py-0 px-2"
|
||||
onclick="event.stopPropagation()">Generator</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-sm btn-outline-danger py-0 px-2"
|
||||
title="Delete"
|
||||
onclick="event.stopPropagation(); openDeleteModal({{ img.path | tojson }}, {{ img.item_name | tojson }})">
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,9 +184,10 @@
|
||||
<div id="lightbox-meta"></div>
|
||||
<div id="lightbox-hint">Click image to open full size · Esc to close</div>
|
||||
</div>
|
||||
<button id="lightbox-prompt-btn" class="btn btn-sm btn-light" onclick="event.stopPropagation(); lightboxShowPrompt()">
|
||||
View Prompt
|
||||
</button>
|
||||
<div id="lightbox-actions" onclick="event.stopPropagation()">
|
||||
<button class="btn btn-sm btn-light" onclick="lightboxShowPrompt()">View Prompt</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="openDeleteModal(_lightboxPath, _lightboxName); closeLightbox()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt modal -->
|
||||
@@ -277,6 +245,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete confirmation modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Delete Image</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-0">Delete <strong id="deleteItemName"></strong>? The image file will be removed.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -379,8 +365,12 @@ async function showPrompt(imgPath, name, category, slug) {
|
||||
// Generator link
|
||||
const genUrl = category === 'characters'
|
||||
? `/character/${slug}`
|
||||
: `/generator?${category.replace(/s$/, '')}=${encodeURIComponent(slug)}`;
|
||||
document.getElementById('openGeneratorBtn').href = genUrl;
|
||||
: category === 'checkpoints'
|
||||
? `/checkpoint/${slug}`
|
||||
: `/generator?${category.replace(/s$/, '')}=${encodeURIComponent(slug)}`;
|
||||
const genBtn = document.getElementById('openGeneratorBtn');
|
||||
genBtn.href = genUrl;
|
||||
genBtn.textContent = (category === 'characters' || category === 'checkpoints') ? 'Open' : 'Open in Generator';
|
||||
} catch (e) {
|
||||
document.getElementById('promptPositive').value = 'Error loading metadata.';
|
||||
} finally {
|
||||
@@ -397,5 +387,46 @@ function copyField(id, btn) {
|
||||
setTimeout(() => btn.textContent = orig, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Delete modal ----
|
||||
let _deletePath = '';
|
||||
let deleteModal;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
});
|
||||
|
||||
function openDeleteModal(path, name) {
|
||||
_deletePath = path;
|
||||
document.getElementById('deleteItemName').textContent = name;
|
||||
deleteModal.show();
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
deleteModal.hide();
|
||||
try {
|
||||
const res = await fetch('/gallery/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({path: _deletePath}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok') {
|
||||
const card = document.querySelector(`.gallery-card[data-path="${CSS.escape(_deletePath)}"]`);
|
||||
if (card) card.remove();
|
||||
const countEl = document.querySelector('h4 .text-muted');
|
||||
if (countEl) {
|
||||
const m = countEl.textContent.match(/(\d+)/);
|
||||
if (m) {
|
||||
const n = parseInt(m[1]) - 1;
|
||||
countEl.textContent = ` ${n} image${n !== 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert('Delete failed: ' + (data.error || 'unknown error'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Delete failed: ' + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user