You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RedditVideoMakerBot/GUI/create.html

572 lines
28 KiB

{% extends "layout.html" %}
{% block main %}
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-12">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 lg:grid-cols-5 gap-6">
<!-- LEFT PANEL: Controls + Progress -->
<div class="lg:col-span-2">
<div class="card-neo">
<div class="p-8">
<div class="flex items-center gap-4 mb-8">
<div class="bg-[#DE6C56]/10 p-3 border border-[#DE6C56]">
<i data-lucide="plus-square" class="w-8 h-8 text-[#DE6C56]"></i>
</div>
<div>
<h2 class="font-display font-black uppercase tracking-tighter text-2xl text-[#111111]">Create New Short</h2>
<p class="text-[#111111]/50 font-mono text-xs mt-1">Start the automated video creation pipeline.</p>
</div>
</div>
<div class="space-y-6">
<!-- Search Keywords Input -->
<div class="form-control w-full">
<label class="label px-0">
<span class="label-text text-[#111111] font-mono text-xs uppercase tracking-wider font-medium">Search Keywords</span>
<span class="label-text-alt text-[#111111]/40 font-mono text-[10px]">Optional</span>
</label>
<div class="flex gap-2">
<input id="keywords-input" type="text"
class="input-neo flex-1"
placeholder="news, politics, trending, viral"
value="{{ default_search_queries }}"
data-demo-disabled>
<button id="clear-keywords" class="btn-ghost-neo"
onclick="document.getElementById('keywords-input').value=''" type="button"
data-demo-disabled>
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<label class="label px-0">
<span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated topics. Leave empty for config default.</span>
</label>
</div>
<!-- Action Button -->
<button id="create-btn" class="btn-primary-neo w-full h-16 text-lg"
onclick="startPipeline()" disabled>
<span id="btn-text">Initializing...</span>
<span id="btn-spinner" class="loading loading-spinner loading-md hidden"></span>
</button>
<!-- Progress Visualization -->
<div id="progress-area" class="hidden space-y-4">
<div class="flex justify-between items-end">
<div class="space-y-1">
<span class="text-xs uppercase tracking-widest text-[#111111]/40 font-mono font-bold">Current Stage</span>
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-[#DE6C56]"></div>
<h3 id="stage-text" class="text-[#DE6C56] font-mono font-bold text-lg uppercase tracking-wider">Preparing...</h3>
</div>
</div>
<span id="pct-text" class="text-2xl font-black text-[#111111]/20 font-mono">0%</span>
</div>
<progress id="progress-bar" class="progress w-full h-3 bg-[#111111]/10 [&::-webkit-progress-value]:bg-[#DE6C56] [&::-moz-progress-bar]:bg-[#DE6C56]" value="0" max="100"></progress>
<div class="grid grid-cols-2 gap-4 pt-4">
<div class="bg-white border border-[#111111] p-3 flex items-center gap-3">
<i data-lucide="clock" class="w-4 h-4 text-[#111111]/40"></i>
<div class="flex flex-col">
<span class="text-[10px] uppercase text-[#111111]/40 font-mono font-bold">Elapsed</span>
<span id="elapsed-time" class="text-sm font-mono text-[#111111]">00:00</span>
</div>
</div>
<div class="bg-white border border-[#111111] p-3 flex items-center gap-3">
<i data-lucide="layers" class="w-4 h-4 text-[#111111]/40"></i>
<div class="flex flex-col">
<span class="text-[10px] uppercase text-[#111111]/40 font-mono font-bold">Status</span>
<span class="text-sm font-mono text-[#DE6C56]">Processing</span>
</div>
</div>
</div>
</div>
<!-- Success Message -->
<div id="done-area" class="hidden">
<div class="border border-[#4ADE80] bg-[#4ADE80]/10 p-6 flex flex-col items-start gap-4">
<div class="flex items-center gap-3">
<div class="bg-[#4ADE80] text-[#111111] p-1">
<i data-lucide="check" class="w-4 h-4"></i>
</div>
<span class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Generation Complete!</span>
</div>
<p id="done-msg" class="text-[#111111]/60 font-mono text-sm">Your video has been rendered and saved to the library.</p>
<a href="{{ app_url('/') }}" class="btn-secondary-neo text-sm">View Video</a>
</div>
</div>
<!-- Error Message -->
<div id="error-area" class="hidden">
<div class="border border-[#DE6C56] bg-[#DE6C56]/5 p-6">
<i data-lucide="alert-triangle" class="w-6 h-6 text-[#DE6C56]"></i>
<div class="mt-2">
<h3 class="font-display font-black uppercase tracking-tighter text-[#111111]">Pipeline Failed</h3>
<div id="error-text" class="text-xs mt-2 font-mono bg-[#111111]/5 p-3 overflow-x-auto whitespace-pre-wrap text-[#111111]/70"></div>
</div>
</div>
</div>
<!-- Log Output -->
<div id="log-area" class="hidden space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-xs uppercase tracking-widest text-[#111111]/40 font-mono font-bold">Execution Logs</h4>
<span class="font-mono text-[10px] uppercase border border-[#111111] px-2 py-0.5 text-[#111111]/50">Real-time</span>
</div>
<div id="log-list" class="bg-white border border-[#111111] p-4 font-mono text-[11px] leading-relaxed text-[#111111]/60 h-48 overflow-y-auto">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- RIGHT PANEL: Pipeline Activity Visualization -->
<div class="lg:col-span-3">
<div class="card-neo">
<div class="p-6">
<div class="flex items-center justify-between mb-5">
<div class="flex items-center gap-2">
<i data-lucide="activity" class="w-5 h-5 text-[#DE6C56]"></i>
<h3 class="font-display font-black uppercase tracking-tighter text-lg text-[#111111]">Pipeline Activity</h3>
</div>
<span class="font-mono text-[10px] uppercase border border-[#4ADE80] text-[#4ADE80] px-2 py-0.5">Live</span>
</div>
<!-- Stage Diagram -->
<div id="stage-diagram" class="mb-6">
</div>
<!-- Scraper Event Feed -->
<div id="scraper-feed" class="space-y-2 max-h-[500px] overflow-y-auto pr-1">
</div>
<!-- Empty state -->
<div id="feed-empty" class="text-center py-16 text-[#111111]/30">
<i data-lucide="eye-off" class="w-12 h-12 mx-auto mb-3"></i>
<p class="text-sm font-mono uppercase tracking-wider">Scraper activity will appear here</p>
<p class="text-xs mt-1 font-mono">Start a pipeline to see real-time scraping visualization</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let pollTimer = null;
let startTime = null;
let elapsedTimer = null;
const stageWeights = {
'configuring': 5,
'discovering': 15,
'scraping': 20,
'fetching': 25,
'saving': 35,
'tts': 45,
'screenshots': 60,
'background': 70,
'chopping': 75,
'creating': 80,
'rendering': 90,
'done': 100,
'error': 0
};
const pipelineStages = [
{ id: 'configuring', label: 'Configuring', icon: 'settings' },
{ id: 'discovering', label: 'Discovering', icon: 'search' },
{ id: 'scraping', label: 'Scraping', icon: 'loader-2' },
{ id: 'fetching', label: 'Fetching', icon: 'download' },
{ id: 'tts', label: 'TTS', icon: 'volume-2' },
{ id: 'screenshots', label: 'Screenshots', icon: 'camera' },
{ id: 'background', label: 'Background', icon: 'image' },
{ id: 'chopping', label: 'Chopping', icon: 'scissors' },
{ id: 'creating', label: 'Creating', icon: 'film' },
{ id: 'rendering', label: 'Rendering', icon: 'sparkles' },
{ id: 'done', label: 'Complete', icon: 'check-circle' },
];
// --- helpers ---
function esc(s) {
if (!s) return '';
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function fmtMetric(n) {
if (!n && n !== 0) return '0';
n = Number(n);
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return n.toLocaleString();
}
function fmtTime(ts) {
if (!ts) return '';
const seconds = Math.floor((Date.now() / 1000) - ts);
if (seconds < 5) return 'just now';
if (seconds < 60) return seconds + 's ago';
const mins = Math.floor(seconds / 60);
return mins + 'm ago';
}
function updateElapsedTime() {
if (!startTime) return;
const now = new Date();
const diff = Math.floor((now - startTime) / 1000);
const mins = Math.floor(diff / 60).toString().padStart(2, '0');
const secs = (diff % 60).toString().padStart(2, '0');
document.getElementById('elapsed-time').textContent = mins + ':' + secs;
}
function stageProgress(stage) {
let pct = 0;
const s = (stage || '').toLowerCase();
for (let [key, val] of Object.entries(stageWeights)) {
if (s.includes(key)) { pct = val; }
}
return pct;
}
// --- stage diagram ---
function renderStageDiagram(currentStage) {
const container = document.getElementById('stage-diagram');
const s = (currentStage || '').toLowerCase();
let activeIdx = -1;
for (let i = 0; i < pipelineStages.length; i++) {
if (s.includes(pipelineStages[i].id)) {
activeIdx = i;
}
}
const start = Math.max(0, activeIdx - 4);
const end = Math.min(pipelineStages.length - 1, Math.max(activeIdx, start + 7));
const visible = pipelineStages.slice(start, end + 1);
let html = '<div class="flex items-center gap-1 overflow-x-auto py-2">';
visible.forEach((st, idx) => {
const globalIdx = start + idx;
const isDone = globalIdx < activeIdx;
const isActive = globalIdx === activeIdx;
const isPending = globalIdx > activeIdx;
let cls = 'flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-mono font-medium whitespace-nowrap shrink-0 transition-all duration-300 border ';
if (isActive) {
cls += 'bg-[#DE6C56]/10 border-[#DE6C56] text-[#DE6C56]';
} else if (isDone) {
cls += 'bg-[#4ADE80]/10 border-[#4ADE80] text-[#111111]';
} else {
cls += 'bg-white border-[#111111]/20 text-[#111111]/40';
}
html += '<div class="' + cls + '">';
if (isDone) {
html += '<i data-lucide="check" class="w-3 h-3 text-[#4ADE80]"></i>';
} else if (isActive) {
html += '<i data-lucide="loader-2" class="w-3 h-3 text-[#DE6C56] animate-spin"></i>';
} else {
html += '<i data-lucide="' + st.icon + '" class="w-3 h-3"></i>';
}
html += '<span class="uppercase tracking-wider">' + st.label + '</span></div>';
if (idx < visible.length - 1) {
html += '<div class="text-[#111111]/30 shrink-0 mx-0.5">///</div>';
}
});
html += '</div>';
container.innerHTML = html;
lucide.createIcons();
}
// --- scraper event feed ---
function renderScraperEvent(event) {
const { type, data, ts } = event;
const d = data || {};
const templates = {
'post_discovered': () =>
'<div class="bg-white border border-[#111111] p-3 hover:border-[#DE6C56] transition-colors">' +
'<div class="flex items-start gap-3">' +
'<div class="bg-[#DE6C56]/10 p-1.5 border border-[#DE6C56] shrink-0">' +
'<i data-lucide="message-circle" class="w-3.5 h-3.5 text-[#DE6C56]"></i>' +
'</div>' +
'<div class="flex-1 min-w-0">' +
'<div class="flex items-center gap-2 mb-1">' +
'<span class="text-xs font-mono font-bold text-[#111111] truncate uppercase">' + esc(d.username || 'unknown') + '</span>' +
'<span class="text-[10px] text-[#111111]/40 font-mono">' + fmtTime(ts) + '</span>' +
'</div>' +
'<p class="text-[11px] text-[#111111]/60 font-mono leading-relaxed line-clamp-2">' + esc(d.body || '') + '</p>' +
'<div class="flex items-center gap-3 mt-1.5 text-[10px] text-[#111111]/40 font-mono">' +
'<span>&#9829; ' + fmtMetric(d.likes) + '</span>' +
'<span>&#128172; ' + fmtMetric(d.replies) + '</span>' +
(d.reposts ? '<span>&#128259; ' + fmtMetric(d.reposts) + '</span>' : '') +
'</div>' +
'</div>' +
'</div>' +
'</div>',
'feed_scroll': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="mouse-pointer-2" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' +
'Scrolled <strong class="text-[#111111]">' + (d.scroll || '?') + '/' + (d.max_scrolls || '?') + '</strong>' +
' &mdash; <strong class="text-[#111111]">' + (d.new_posts || 0) + '</strong> new,' +
' <strong class="text-[#111111]">' + (d.total_posts || 0) + '</strong> total posts' +
'</span>' +
'</div>',
'search_query': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="search" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' +
'Searched "<strong class="text-[#111111]">' + esc(d.query || '') + '</strong>"' +
' &mdash; <strong class="text-[#111111]">' + (d.posts_found || 0) + '</strong> posts found' +
'</span>' +
'</div>',
'filter_results': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="filter" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' +
'Filtered <strong class="text-[#111111]">' + (d.before || 0) + '</strong> posts &rarr;' +
' <strong class="text-[#111111]">' + (d.after || 0) + '</strong> candidates' +
(d.min_engagement ? ' (min ' + fmtMetric(d.min_engagement) + ' engagement)' : '') +
'</span>' +
'</div>',
'visiting_post': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="external-link" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' +
'Examining candidate #' + (d.attempt || '?') + ': "' + esc((d.body || '').substring(0, 40)) + '..."' +
' <span class="text-[#111111]/40 ml-1">&#9829;' + fmtMetric(d.likes) + '</span>' +
'</span>' +
'</div>',
'replies_found': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="message-square" class="w-3 h-3 text-[#4ADE80]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' +
d.count + ' replies found' + (d.min_required ? ' (need ' + d.min_required + ')' : '') +
'</span>' +
'</div>',
'post_selected': () =>
'<div class="bg-[#4ADE80]/10 border border-[#4ADE80] p-3">' +
'<div class="flex items-start gap-3">' +
'<div class="bg-[#4ADE80] p-1.5 shrink-0">' +
'<i data-lucide="check-circle" class="w-3.5 h-3.5 text-[#111111]"></i>' +
'</div>' +
'<div>' +
'<span class="text-xs font-mono font-bold text-[#111111] uppercase">Post Selected!</span>' +
'<p class="text-[11px] text-[#111111]/60 font-mono mt-0.5">' + esc(d.title || '') + '</p>' +
'<div class="flex gap-3 mt-1 text-[10px] text-[#111111]/40 font-mono">' +
'<span>&#9829; ' + fmtMetric(d.likes) + '</span>' +
'<span>&#128172; ' + (d.replies_count || 0) + ' replies</span>' +
'</div>' +
'</div>' +
'</div>' +
'</div>',
'login': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="log-in" class="w-3 h-3 text-[#111111]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || '') + '</span>' +
'</div>',
'browser_launch': () =>
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<div class="bg-white border border-[#111111] p-1">' +
'<i data-lucide="globe" class="w-3 h-3 text-[#DE6C56]"></i>' +
'</div>' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || '') + '</span>' +
'</div>',
};
const fn = templates[type];
return fn ? fn() : (
'<div class="flex items-center gap-2 py-1.5 px-1 font-mono">' +
'<span class="text-[11px] text-[#111111]/50">' + esc(d.message || type) + '</span>' +
'</div>'
);
}
function renderScraperFeed(events) {
const container = document.getElementById('scraper-feed');
const empty = document.getElementById('feed-empty');
if (!events || events.length === 0) {
container.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
const recent = events.slice(-50);
container.innerHTML = recent.map(function(e) { return renderScraperEvent(e); }).join('');
container.scrollTop = container.scrollHeight;
lucide.createIcons();
}
// --- pipeline lifecycle ---
async function startPipeline() {
const btn = document.getElementById('create-btn');
const btnText = document.getElementById('btn-text');
const spinner = document.getElementById('btn-spinner');
btn.disabled = true;
spinner.classList.remove('hidden');
btnText.textContent = 'Initializing...';
document.getElementById('progress-area').classList.remove('hidden');
document.getElementById('log-area').classList.remove('hidden');
document.getElementById('done-area').classList.add('hidden');
document.getElementById('error-area').classList.add('hidden');
// Show empty feed state
document.getElementById('scraper-feed').innerHTML = '';
document.getElementById('feed-empty').classList.remove('hidden');
document.getElementById('stage-diagram').innerHTML = '';
// Reset progress
document.getElementById('progress-bar').classList.remove('progress-success', 'progress-error');
const keywords = document.getElementById('keywords-input').value.trim();
try {
const r = await fetch(window.appPath('/create'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ search_queries: keywords || null }),
});
const data = await r.json();
if (data.status === 'started' || data.status === 'already_running') {
btnText.textContent = 'Processing...';
startTime = new Date();
elapsedTimer = setInterval(updateElapsedTime, 1000);
pollTimer = setInterval(pollStatus, 2000);
}
} catch (err) {
console.error("Failed to start pipeline:", err);
btn.disabled = false;
spinner.classList.add('hidden');
btnText.textContent = 'Retry';
}
}
async function pollStatus() {
try {
const r = await fetch(window.appPath('/create/status'));
const state = await r.json();
const stageText = document.getElementById('stage-text');
const progressBar = document.getElementById('progress-bar');
const pctText = document.getElementById('pct-text');
const logList = document.getElementById('log-list');
stageText.textContent = state.stage || 'Running...';
const pct = stageProgress(state.stage || '');
progressBar.value = pct;
pctText.textContent = pct + '%';
if (state.log && state.log.length > 0) {
logList.innerHTML = state.log.map(function(l) {
return '<div class="py-0.5 border-b border-[#111111]/10 last:border-0">' + esc(l) + '</div>';
}).join('');
logList.scrollTop = logList.scrollHeight;
}
// Render visualization panels
renderStageDiagram(state.stage);
if (state.scraper_events) {
renderScraperFeed(state.scraper_events);
}
if (!state.running) {
clearInterval(pollTimer);
clearInterval(elapsedTimer);
pollTimer = null;
document.getElementById('btn-spinner').classList.add('hidden');
if (state.stage === 'done' || state.result) {
progressBar.value = 100;
progressBar.classList.add('progress-success');
document.getElementById('btn-text').textContent = 'Create New';
document.getElementById('done-area').classList.remove('hidden');
if (state.result) {
document.getElementById('done-msg').textContent = state.result.message;
}
} else if (state.error) {
progressBar.classList.add('progress-error');
document.getElementById('btn-text').textContent = 'Retry';
document.getElementById('error-area').classList.remove('hidden');
document.getElementById('error-text').textContent = state.error;
}
document.getElementById('create-btn').disabled = false;
}
} catch (err) {
console.error("Status poll failed:", err);
}
}
// --- init ---
window.addEventListener('load', async function() {
lucide.createIcons();
try {
const r = await fetch(window.appPath('/create/status'));
const state = await r.json();
const btn = document.getElementById('create-btn');
if (window.PUBLIC_DEMO_MODE) {
btn.disabled = true;
document.getElementById('btn-text').textContent = 'Public Demo: Disabled';
} else if (state.running) {
document.getElementById('progress-area').classList.remove('hidden');
document.getElementById('log-area').classList.remove('hidden');
btn.disabled = true;
document.getElementById('btn-spinner').classList.remove('hidden');
document.getElementById('btn-text').textContent = 'Running...';
startTime = new Date();
elapsedTimer = setInterval(updateElapsedTime, 1000);
pollTimer = setInterval(pollStatus, 2000);
renderStageDiagram(state.stage);
if (state.scraper_events) {
renderScraperFeed(state.scraper_events);
}
} else {
btn.disabled = false;
document.getElementById('btn-text').textContent = 'Start Generation';
}
} catch (err) {
console.error("Initial status check failed:", err);
document.getElementById('create-btn').disabled = window.PUBLIC_DEMO_MODE;
document.getElementById('btn-text').textContent = window.PUBLIC_DEMO_MODE ? 'Public Demo: Disabled' : 'Start Generation';
}
});
</script>
{% endblock %}