💬 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:
Adam Ladachowski
2026-02-14 03:41:07 +01:00
parent 1cad0fea94
commit 52b39a4f6b
+233 -11
View File
@@ -226,6 +226,76 @@
.empty { text-align: center; padding: 4rem 1rem; color: #666; }
.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 {
display: flex;
@@ -540,10 +610,46 @@
<!-- Generate View -->
<div id="generate-view" class="view active">
<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-row">
<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>
</div>
</div>
@@ -652,7 +758,95 @@
const chatArea = document.getElementById('chat-area');
const promptInput = document.getElementById('prompt-input');
const stepsInput = document.getElementById('steps-input');
const batchInput = document.getElementById('batch-input');
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 => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -667,56 +861,84 @@
if (!prompt) return;
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 = '';
generateBtn.disabled = true;
// Add message
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message';
const resLabel = `${width}×${height}`;
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="generating">Generating</div>
${Array(batch).fill('<div class="generating">Generating</div>').join('')}
</div>
`;
chatArea.appendChild(msgDiv);
chatArea.scrollTop = chatArea.scrollHeight;
const imagesDiv = msgDiv.querySelector('.chat-images');
let allImages = [];
try {
// Generate batch sequentially
for (let i = 0; i < batch; i++) {
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: prompt,
prompt: finalPrompt,
steps: steps,
width: 512,
height: 512,
width: width,
height: height,
seed: -1,
save_to_gallery: true
})
});
const data = await res.json();
const imagesDiv = msgDiv.querySelector('.chat-images');
if (data.images && data.images.length > 0) {
imagesDiv.innerHTML = data.images.map(img => `
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('');
`).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>`;
} else {
break;
}
chatArea.scrollTop = chatArea.scrollHeight;
}
if (allImages.length === 0) {
imagesDiv.innerHTML = '<div class="empty error">Generation failed</div>';
}
} 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;
chatArea.scrollTop = chatArea.scrollHeight;
}
// Load models on page load
loadModels();
// Search
const searchInput = document.getElementById('search-input');
const searchType = document.getElementById('search-type');