5d60d33db1
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
171 lines
6.7 KiB
HTML
171 lines
6.7 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}AI Studio · ComfyUI{% endblock %}
|
||
{% block extra_head %}
|
||
<style>
|
||
.studio-grid { display: grid; grid-template-columns: 1fr 340px; gap: 1rem; }
|
||
@media (max-width: 1000px) { .studio-grid { grid-template-columns: 1fr; } }
|
||
.studio-preview {
|
||
min-height: 420px; display: flex; align-items: center; justify-content: center;
|
||
background: rgba(0,0,0,.35); border: 1px solid rgba(148,163,184,.15);
|
||
border-radius: 12px; overflow: hidden;
|
||
}
|
||
.studio-preview img { max-width: 100%; max-height: 70vh; border-radius: 8px; }
|
||
.progress-bar {
|
||
height: 8px; background: rgba(148,163,184,.15); border-radius: 999px; overflow: hidden;
|
||
margin: .75rem 0;
|
||
}
|
||
.progress-fill {
|
||
height: 100%; width: 0%; background: linear-gradient(90deg, #22d3ee, #a855f7);
|
||
transition: width .4s ease;
|
||
}
|
||
.event-log {
|
||
max-height: 280px; overflow-y: auto; font-family: ui-monospace, monospace;
|
||
font-size: .78rem; background: rgba(0,0,0,.35); border-radius: 8px; padding: .6rem;
|
||
border: 1px solid rgba(148,163,184,.12);
|
||
}
|
||
.event-log div { padding: .15rem 0; color: var(--text-secondary); }
|
||
.quality-pills { display: flex; gap: .5rem; flex-wrap: wrap; margin: .75rem 0; }
|
||
.quality-pill {
|
||
padding: .35rem .75rem; border-radius: 999px; cursor: pointer; font-size: .85rem;
|
||
border: 1px solid rgba(148,163,184,.2); background: rgba(0,0,0,.2);
|
||
}
|
||
.quality-pill.active { border-color: var(--accent-cyan); color: var(--accent-cyan); }
|
||
.studio-links { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: .75rem; }
|
||
</style>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div x-data="aiStudio()" x-init="init()">
|
||
<header class="page-header">
|
||
<h1>AI Studio</h1>
|
||
<p class="subtitle page-tip">ComfyUI — prompt, live voortgang, HD export. CPU: 3–10 min per HD foto.</p>
|
||
</header>
|
||
|
||
<div class="studio-grid">
|
||
<section class="panel">
|
||
<h2 style="font-size:1rem;margin:0 0 .75rem;">Positieve prompt</h2>
|
||
<textarea class="form-input" rows="3" x-model="prompt"
|
||
placeholder="bijv. premium halal kipnuggets productfoto, studio softbox, witte achtergrond, 8k commercial"></textarea>
|
||
|
||
<h2 style="font-size:1rem;margin:1rem 0 .75rem;">Negatieve prompt</h2>
|
||
<textarea class="form-input" rows="2" x-model="negativePrompt"
|
||
placeholder="blurry, low quality, watermark, text, ugly, deformed, bad anatomy"></textarea>
|
||
|
||
<div class="quality-pills">
|
||
<button type="button" class="quality-pill" :class="{active: quality==='fast'}" @click="quality='fast'">Snel · 512px</button>
|
||
<button type="button" class="quality-pill" :class="{active: quality==='hd'}" @click="quality='hd'">HD · 1024px</button>
|
||
<button type="button" class="quality-pill" :class="{active: quality==='ultra'}" @click="quality='ultra'">Ultra · 1024px+</button>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:.5rem;flex-wrap:wrap;">
|
||
<button class="btn btn-primary" @click="start()" :disabled="running || !prompt.trim()">
|
||
<span x-show="!running">Genereer HD foto</span>
|
||
<span x-show="running">Bezig…</span>
|
||
</button>
|
||
<a class="btn btn-ghost" href="http://10.4.7.18:8188" target="_blank" rel="noopener">ComfyUI backend ↗</a>
|
||
</div>
|
||
|
||
<div x-show="running || percent > 0" style="margin-top:1rem;">
|
||
<div style="display:flex;justify-content:space-between;font-size:.85rem;">
|
||
<span x-text="statusLabel"></span>
|
||
<span x-text="percent + '%'"></span>
|
||
</div>
|
||
<div class="progress-bar"><div class="progress-fill" :style="'width:' + percent + '%'"></div></div>
|
||
<p class="page-tip" x-text="statusMessage" style="margin:0;"></p>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="panel">
|
||
<h2 style="font-size:1rem;margin:0 0 .75rem;">Live log</h2>
|
||
<div class="event-log">
|
||
<template x-for="(ev, i) in events" :key="i">
|
||
<div x-text="ev"></div>
|
||
</template>
|
||
<div x-show="!events.length" class="page-tip">Start een generatie om ComfyUI live te volgen.</div>
|
||
</div>
|
||
<div class="studio-links" x-show="imageUrl">
|
||
<a class="btn btn-ghost" :href="imageUrl" target="_blank" download>Download</a>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<div class="panel" style="margin-top:1rem;">
|
||
<div class="studio-preview">
|
||
<img x-show="imageUrl" :src="imageUrl" alt="Gegenereerde afbeelding" />
|
||
<p x-show="!imageUrl && !running" class="page-tip">Nog geen afbeelding — vul prompt in en klik Genereer.</p>
|
||
<p x-show="running && !imageUrl" class="page-tip">ComfyUI werkt op de achtergrond…</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
{% block scripts %}
|
||
<script>
|
||
function aiStudio() {
|
||
return {
|
||
prompt: '',
|
||
negativePrompt: 'blurry, low quality, watermark, text, ugly, deformed, bad anatomy, oversaturated',
|
||
quality: 'hd',
|
||
running: false,
|
||
promptId: null,
|
||
pollTimer: null,
|
||
percent: 0,
|
||
statusLabel: '',
|
||
statusMessage: '',
|
||
events: [],
|
||
imageUrl: '',
|
||
init() {},
|
||
async start() {
|
||
if (!this.prompt.trim() || this.running) return;
|
||
this.running = true;
|
||
this.percent = 0;
|
||
this.events = [];
|
||
this.imageUrl = '';
|
||
this.statusLabel = 'Starten…';
|
||
this.statusMessage = 'ComfyUI queue';
|
||
try {
|
||
const r = await Cockpit.api('/api/ai/generate-image/start', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ prompt: this.prompt.trim(), negative_prompt: this.negativePrompt.trim(), quality: this.quality })
|
||
});
|
||
this.promptId = r.prompt_id;
|
||
this.events = r.events || ['Gestart'];
|
||
this.statusLabel = r.quality_label || this.quality;
|
||
this.poll();
|
||
} catch (e) {
|
||
Cockpit.toast(e.message, 'error');
|
||
this.running = false;
|
||
}
|
||
},
|
||
poll() {
|
||
if (this.pollTimer) clearInterval(this.pollTimer);
|
||
this.pollTimer = setInterval(async () => {
|
||
if (!this.promptId) return;
|
||
try {
|
||
const j = await Cockpit.api('/api/ai/generate-image/progress/' + this.promptId);
|
||
this.percent = j.percent || 0;
|
||
this.statusMessage = j.message || j.status || '';
|
||
this.statusLabel = (j.quality || this.quality) + ' · ' + (j.status || '');
|
||
if (j.events && j.events.length) this.events = j.events;
|
||
if (j.status === 'done' && j.proxy_url) {
|
||
this.imageUrl = j.proxy_url;
|
||
this.percent = 100;
|
||
this.running = false;
|
||
clearInterval(this.pollTimer);
|
||
Cockpit.toast('HD foto klaar', 'success');
|
||
}
|
||
if (j.status === 'error') {
|
||
this.running = false;
|
||
clearInterval(this.pollTimer);
|
||
Cockpit.toast(j.error || 'ComfyUI fout', 'error');
|
||
}
|
||
} catch (e) {
|
||
this.running = false;
|
||
clearInterval(this.pollTimer);
|
||
Cockpit.toast(e.message, 'error');
|
||
}
|
||
}, 1500);
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
{% endblock %}
|