💬 Commit message: Update 2026-02-14 03:41:07, 1 files, 278 lines
📁 Files changed: 1 📝 Lines changed: 278 • index.html
This commit is contained in:
@@ -226,6 +226,76 @@
|
|||||||
.empty { text-align: center; padding: 4rem 1rem; color: #666; }
|
.empty { text-align: center; padding: 4rem 1rem; color: #666; }
|
||||||
.error { color: #dc2626; }
|
.error { color: #dc2626; }
|
||||||
|
|
||||||
|
/* Generate controls */
|
||||||
|
.generate-controls {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #0f0f0f;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.25rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.control-group label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.control-group select,
|
||||||
|
.control-group input[type="number"] {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e5e5e5;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
height: 46px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.control-group select:focus,
|
||||||
|
.control-group input[type="number"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4ade80;
|
||||||
|
}
|
||||||
|
.control-group select { min-width: 120px; cursor: pointer; }
|
||||||
|
.control-group input[type="number"] { width: 65px; text-align: center; }
|
||||||
|
|
||||||
|
/* Large styled selects (like prompt input) */
|
||||||
|
.control-group select.styled-select {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resolution buttons */
|
||||||
|
.res-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.res-btn {
|
||||||
|
padding: 0 0.9rem;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
height: 46px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.res-btn:hover { border-color: #4ade80; color: #e5e5e5; }
|
||||||
|
.res-btn.active { background: #4ade80; color: #0f0f0f; border-color: #4ade80; }
|
||||||
|
|
||||||
/* Search view */
|
/* Search view */
|
||||||
#search-view {
|
#search-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -540,10 +610,46 @@
|
|||||||
<!-- Generate View -->
|
<!-- Generate View -->
|
||||||
<div id="generate-view" class="view active">
|
<div id="generate-view" class="view active">
|
||||||
<div class="chat-area" id="chat-area"></div>
|
<div class="chat-area" id="chat-area"></div>
|
||||||
|
<div class="generate-controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Model</label>
|
||||||
|
<select id="checkpoint-select" class="styled-select"><option value="">Loading...</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>LoRA</label>
|
||||||
|
<select id="lora-select" class="styled-select"><option value="">None</option></select>
|
||||||
|
<input type="number" id="lora-weight" value="0.8" min="0" max="2" step="0.1" title="LoRA weight">
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Size</label>
|
||||||
|
<div class="res-size-group">
|
||||||
|
<div class="res-buttons" id="res-base-btns">
|
||||||
|
<button class="res-btn" data-base="512">512</button>
|
||||||
|
<button class="res-btn active" data-base="768">768</button>
|
||||||
|
<button class="res-btn" data-base="1024">1024</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Ratio</label>
|
||||||
|
<div class="res-buttons" id="res-ratio-btns">
|
||||||
|
<button class="res-btn" data-ratio="3:4" title="Portrait">3:4</button>
|
||||||
|
<button class="res-btn active" data-ratio="1:1" title="Square">1:1</button>
|
||||||
|
<button class="res-btn" data-ratio="4:3" title="Landscape">4:3</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Steps</label>
|
||||||
|
<input type="number" id="steps-input" value="20" min="1" max="50">
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Batch</label>
|
||||||
|
<input type="number" id="batch-input" value="1" min="1" max="8">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input type="text" id="prompt-input" placeholder="Describe what you want to generate..." autofocus>
|
<input type="text" id="prompt-input" placeholder="Describe what you want to generate..." autofocus>
|
||||||
<input type="number" id="steps-input" value="20" min="1" max="50" title="Steps">
|
|
||||||
<button id="generate-btn">Generate</button>
|
<button id="generate-btn">Generate</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -652,7 +758,95 @@
|
|||||||
const chatArea = document.getElementById('chat-area');
|
const chatArea = document.getElementById('chat-area');
|
||||||
const promptInput = document.getElementById('prompt-input');
|
const promptInput = document.getElementById('prompt-input');
|
||||||
const stepsInput = document.getElementById('steps-input');
|
const stepsInput = document.getElementById('steps-input');
|
||||||
|
const batchInput = document.getElementById('batch-input');
|
||||||
const generateBtn = document.getElementById('generate-btn');
|
const generateBtn = document.getElementById('generate-btn');
|
||||||
|
const checkpointSelect = document.getElementById('checkpoint-select');
|
||||||
|
const loraSelect = document.getElementById('lora-select');
|
||||||
|
const loraWeight = document.getElementById('lora-weight');
|
||||||
|
|
||||||
|
// Resolution state
|
||||||
|
let currentBase = 768;
|
||||||
|
let currentRatio = '1:1';
|
||||||
|
|
||||||
|
// Resolution presets: base -> ratio -> [width, height]
|
||||||
|
const resolutions = {
|
||||||
|
512: { '1:1': [512, 512], '4:3': [512, 384], '3:4': [384, 512] },
|
||||||
|
768: { '1:1': [768, 768], '4:3': [768, 576], '3:4': [576, 768] },
|
||||||
|
1024: { '1:1': [1024, 1024], '4:3': [1024, 768], '3:4': [768, 1024] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getResolution() {
|
||||||
|
return resolutions[currentBase][currentRatio];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolution button handlers
|
||||||
|
document.querySelectorAll('#res-base-btns .res-btn').forEach(btn => {
|
||||||
|
btn.onclick = () => {
|
||||||
|
document.querySelectorAll('#res-base-btns .res-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
currentBase = parseInt(btn.dataset.base);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('#res-ratio-btns .res-btn').forEach(btn => {
|
||||||
|
btn.onclick = () => {
|
||||||
|
document.querySelectorAll('#res-ratio-btns .res-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
currentRatio = btn.dataset.ratio;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load models on init
|
||||||
|
async function loadModels() {
|
||||||
|
try {
|
||||||
|
const [modelsRes, lorasRes, activeRes] = await Promise.all([
|
||||||
|
fetch('/api/models'),
|
||||||
|
fetch('/api/models/loras'),
|
||||||
|
fetch('/api/models/active')
|
||||||
|
]);
|
||||||
|
const models = await modelsRes.json();
|
||||||
|
const loras = await lorasRes.json();
|
||||||
|
const active = await activeRes.json();
|
||||||
|
|
||||||
|
// Populate checkpoints
|
||||||
|
checkpointSelect.innerHTML = models.models.map(m =>
|
||||||
|
`<option value="${m.path}" ${m.path === active.model ? 'selected' : ''}>${m.name}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// Populate LoRAs
|
||||||
|
loraSelect.innerHTML = '<option value="">None</option>' +
|
||||||
|
loras.loras.map(l => `<option value="${l.path}">${l.name}</option>`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load models:', e);
|
||||||
|
checkpointSelect.innerHTML = '<option value="">Failed to load</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch checkpoint
|
||||||
|
checkpointSelect.onchange = async () => {
|
||||||
|
const model = checkpointSelect.value;
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
checkpointSelect.disabled = true;
|
||||||
|
generateBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/models/switch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ model })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
alert('Failed to switch model: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Failed to switch model: ' + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpointSelect.disabled = false;
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
promptInput.addEventListener('keydown', e => {
|
promptInput.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
@@ -667,56 +861,84 @@
|
|||||||
if (!prompt) return;
|
if (!prompt) return;
|
||||||
|
|
||||||
const steps = parseInt(stepsInput.value) || 20;
|
const steps = parseInt(stepsInput.value) || 20;
|
||||||
|
const batch = parseInt(batchInput.value) || 1;
|
||||||
|
const [width, height] = getResolution();
|
||||||
|
const lora = loraSelect.value;
|
||||||
|
const weight = parseFloat(loraWeight.value) || 0.8;
|
||||||
|
|
||||||
|
// Build prompt with LoRA if selected
|
||||||
|
let finalPrompt = prompt;
|
||||||
|
if (lora) {
|
||||||
|
const loraName = loraSelect.options[loraSelect.selectedIndex].text;
|
||||||
|
finalPrompt = `<lora:${loraName}:${weight}> ${prompt}`;
|
||||||
|
}
|
||||||
|
|
||||||
promptInput.value = '';
|
promptInput.value = '';
|
||||||
generateBtn.disabled = true;
|
generateBtn.disabled = true;
|
||||||
|
|
||||||
// Add message
|
// Add message
|
||||||
const msgDiv = document.createElement('div');
|
const msgDiv = document.createElement('div');
|
||||||
msgDiv.className = 'chat-message';
|
msgDiv.className = 'chat-message';
|
||||||
|
const resLabel = `${width}×${height}`;
|
||||||
msgDiv.innerHTML = `
|
msgDiv.innerHTML = `
|
||||||
<div class="chat-prompt">${escapeHtml(prompt)}</div>
|
<div class="chat-prompt">${escapeHtml(prompt)} <span style="color:#666;font-size:0.8em">[${resLabel}, ${steps} steps${batch > 1 ? ', batch ' + batch : ''}${lora ? ', +LoRA' : ''}]</span></div>
|
||||||
<div class="chat-images">
|
<div class="chat-images">
|
||||||
<div class="generating">Generating</div>
|
${Array(batch).fill('<div class="generating">Generating</div>').join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
chatArea.appendChild(msgDiv);
|
chatArea.appendChild(msgDiv);
|
||||||
chatArea.scrollTop = chatArea.scrollHeight;
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
|
|
||||||
try {
|
const imagesDiv = msgDiv.querySelector('.chat-images');
|
||||||
const res = await fetch('/api/generate', {
|
let allImages = [];
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: prompt,
|
|
||||||
steps: steps,
|
|
||||||
width: 512,
|
|
||||||
height: 512,
|
|
||||||
save_to_gallery: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
const imagesDiv = msgDiv.querySelector('.chat-images');
|
|
||||||
|
|
||||||
if (data.images && data.images.length > 0) {
|
try {
|
||||||
imagesDiv.innerHTML = data.images.map(img => `
|
// Generate batch sequentially
|
||||||
<a href="javascript:void(0)" class="glightbox" data-type="image" data-href="/api/images/${img.id}">
|
for (let i = 0; i < batch; i++) {
|
||||||
<img src="/api/images/${img.id}" alt="${img.id}">
|
const res = await fetch('/api/generate', {
|
||||||
</a>
|
method: 'POST',
|
||||||
`).join('');
|
headers: { 'Content-Type': 'application/json' },
|
||||||
initLightbox();
|
body: JSON.stringify({
|
||||||
} else if (data.error) {
|
prompt: finalPrompt,
|
||||||
imagesDiv.innerHTML = `<div class="empty error">${escapeHtml(data.error)}</div>`;
|
steps: steps,
|
||||||
} else {
|
width: width,
|
||||||
|
height: height,
|
||||||
|
seed: -1,
|
||||||
|
save_to_gallery: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.images && data.images.length > 0) {
|
||||||
|
allImages.push(...data.images);
|
||||||
|
// Update display progressively
|
||||||
|
imagesDiv.innerHTML = allImages.map(img => `
|
||||||
|
<a href="javascript:void(0)" class="glightbox" data-type="image" data-href="/api/images/${img.id}">
|
||||||
|
<img src="/api/images/${img.id}" alt="${img.id}">
|
||||||
|
</a>
|
||||||
|
`).join('') + (i < batch - 1 ? '<div class="generating">Generating</div>'.repeat(batch - i - 1) : '');
|
||||||
|
initLightbox();
|
||||||
|
} else if (data.error) {
|
||||||
|
imagesDiv.innerHTML = `<div class="empty error">${escapeHtml(data.error)}</div>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allImages.length === 0) {
|
||||||
imagesDiv.innerHTML = '<div class="empty error">Generation failed</div>';
|
imagesDiv.innerHTML = '<div class="empty error">Generation failed</div>';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msgDiv.querySelector('.chat-images').innerHTML = '<div class="empty error">Error: ' + escapeHtml(e.message) + '</div>';
|
imagesDiv.innerHTML = '<div class="empty error">Error: ' + escapeHtml(e.message) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
generateBtn.disabled = false;
|
generateBtn.disabled = false;
|
||||||
chatArea.scrollTop = chatArea.scrollHeight;
|
chatArea.scrollTop = chatArea.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load models on page load
|
||||||
|
loadModels();
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const searchType = document.getElementById('search-type');
|
const searchType = document.getElementById('search-type');
|
||||||
|
|||||||
Reference in New Issue
Block a user