Files
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

171 lines
6.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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: 310 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 %}