Khalil VideoGen — Frontend
/* style.css - minimal clean UI */
:root{
--bg:#0f1720;
--card:#0b1220;
--accent:#4ad2a6;
--muted:#b8c2c9;
--panel:#081018;
--radius:10px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Helvetica,Arial;color:#e6eef6;background:linear-gradient(180deg,#071129 0%, #07132a 100%);-webkit-font-smoothing:antialiased}
.container{max-width:900px;margin:36px auto;padding:20px}
header h1{margin:0;font-size:1.8rem}
.muted{color:var(--muted);margin-top:6px}
.card{background:rgba(255,255,255,0.02);padding:18px;border-radius:var(--radius);box-shadow:0 6px 20px rgba(2,6,23,0.6);margin-top:16px}
label{display:block;font-size:.9rem;color:var(--muted);margin-bottom:6px}
textarea{width:100%;min-height:120px;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:#06101a;color:#e6eef6;resize:vertical}
input[type="text"],input[type="number"]{width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:#06101a;color:#e6eef6}
.row{display:flex;gap:12px}
.col{flex:1}
.actions{display:flex;gap:10px;margin-top:12px}
button{background:linear-gradient(90deg,var(--accent),#2bc6a6);border:0;padding:10px 14px;border-radius:10px;color:#032 ;font-weight:600;cursor:pointer}
button:disabled{opacity:.6;cursor:not-allowed}
.btn-ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}
.status{display:flex;align-items:center;gap:12px;margin-top:12px;color:var(--muted)}
.spinner{width:20px;height:20px;border-radius:50%;border:3px solid rgba(255,255,255,0.06);border-top-color:var(--accent);animation:spin 1s linear infinite}
.hidden{display:none}
.output-row{display:flex;gap:12px;align-items:center}
.links a{display:inline-block;padding:8px 10px;background:rgba(255,255,255,0.03);border-radius:8px;color:var(--accent);text-decoration:none;margin-right:8px}
.preview{width:100%;max-height:480px;margin-top:12px;border-radius:8px;background:#000}
.log{background:#051018;color:var(--muted);padding:10px;border-radius:8px;margin-top:12px;white-space:pre-wrap}
.small{font-size:.85rem}
@keyframes spin{to{transform:rotate(360deg)}}
// script.js - handles UI and requests
(() => {
const form = document.getElementById('genForm');
const genBtn = document.getElementById('genBtn');
const cancelBtn = document.getElementById('cancelBtn');
const statusText = document.getElementById('statusText');
const spinner = document.getElementById('spinner');
const outputSection = document.getElementById('output');
const linksDiv = document.getElementById('links');
const previewVideo = document.getElementById('preview');
const logPre = document.getElementById('log');
let controller = null;
function setBusy(yes, text = '') {
if (yes) {
spinner.classList.remove('hidden');
genBtn.disabled = true;
cancelBtn.hidden = false;
statusText.textContent = text || 'Generating — this may take a few minutes (model download on first run)...';
} else {
spinner.classList.add('hidden');
genBtn.disabled = false;
cancelBtn.hidden = true;
statusText.textContent = text || 'Ready.';
}
}
cancelBtn.addEventListener('click', () => {
if (controller) {
controller.abort();
setBusy(false, 'Generation cancelled.');
controller = null;
}
});
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
outputSection.classList.add('hidden');
linksDiv.innerHTML = '';
previewVideo.src = '';
logPre.textContent = '';
const prompt = document.getElementById('prompt').value.trim();
const duration = document.getElementById('duration').value;
const voice_text = document.getElementById('voice_text').value.trim();
if (!prompt) { statusText.textContent = 'Please enter a prompt.'; return; }
const formData = new FormData();
formData.append('prompt', prompt);
formData.append('duration', duration);
formData.append('voice_text', voice_text);
controller = new AbortController();
setBusy(true, 'Sending request to server...');
try {
const resp = await fetch('/generate', {
method: 'POST',
body: formData,
signal: controller.signal,
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({error: 'Unknown error'}));
setBusy(false, `Server error: ${err.error || resp.statusText}`);
logPre.textContent = JSON.stringify(err, null, 2);
return;
}
const data = await resp.json();
setBusy(false, 'Generation finished.');
// Show download link and video preview
const dl = data.download_url;
const job = data.job_id;
const file = data.file;
linksDiv.innerHTML = `
Download ${file}
(or path: outputs/${job}/${file})`;
previewVideo.src = dl;
previewVideo.classList.remove('hidden');
previewVideo.load();
outputSection.classList.remove('hidden');
logPre.textContent = `Job: ${job}\nFile: ${file}\nDownload URL: ${dl}`;
} catch (err) {
if (err.name === 'AbortError') {
setBusy(false, 'Request aborted.');
logPre.textContent = 'Generation aborted by user.';
} else {
setBusy(false, 'Request failed.');
logPre.textContent = String(err);
}
} finally {
controller = null;
}
});
})();
No comments:
Post a Comment