diff --git a/tensors/server/static/index.html b/tensors/server/static/index.html
index d0262a8..f4fe379 100644
--- a/tensors/server/static/index.html
+++ b/tensors/server/static/index.html
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 =>
+ `
`
+ ).join('');
+
+ // Populate LoRAs
+ loraSelect.innerHTML = '
' +
+ loras.loras.map(l => `
`).join('');
+ } catch (e) {
+ console.error('Failed to load models:', e);
+ checkpointSelect.innerHTML = '
';
+ }
+ }
+
+ // 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 = `
${prompt}`;
+ }
+
promptInput.value = '';
generateBtn.disabled = true;
// Add message
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message';
+ const resLabel = `${width}×${height}`;
msgDiv.innerHTML = `
- ${escapeHtml(prompt)}
+ ${escapeHtml(prompt)} [${resLabel}, ${steps} steps${batch > 1 ? ', batch ' + batch : ''}${lora ? ', +LoRA' : ''}]
-
Generating
+ ${Array(batch).fill('
Generating
').join('')}
`;
chatArea.appendChild(msgDiv);
chatArea.scrollTop = chatArea.scrollHeight;
- try {
- const res = await fetch('/api/generate', {
- 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');
+ const imagesDiv = msgDiv.querySelector('.chat-images');
+ let allImages = [];
- if (data.images && data.images.length > 0) {
- imagesDiv.innerHTML = data.images.map(img => `
-
-
-
- `).join('');
- initLightbox();
- } else if (data.error) {
- imagesDiv.innerHTML = `${escapeHtml(data.error)}
`;
- } else {
+ 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: finalPrompt,
+ steps: steps,
+ 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 => `
+
+
+
+ `).join('') + (i < batch - 1 ? 'Generating
'.repeat(batch - i - 1) : '');
+ initLightbox();
+ } else if (data.error) {
+ imagesDiv.innerHTML = `${escapeHtml(data.error)}
`;
+ break;
+ }
+ chatArea.scrollTop = chatArea.scrollHeight;
+ }
+
+ if (allImages.length === 0) {
imagesDiv.innerHTML = 'Generation failed
';
}
} catch (e) {
- msgDiv.querySelector('.chat-images').innerHTML = 'Error: ' + escapeHtml(e.message) + '
';
+ imagesDiv.innerHTML = 'Error: ' + escapeHtml(e.message) + '
';
}
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');