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.
572 lines
28 KiB
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>♥ ' + fmtMetric(d.likes) + '</span>' +
|
|
'<span>💬 ' + fmtMetric(d.replies) + '</span>' +
|
|
(d.reposts ? '<span>🔃 ' + 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>' +
|
|
' — <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>"' +
|
|
' — <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 →' +
|
|
' <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">♥' + 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>♥ ' + fmtMetric(d.likes) + '</span>' +
|
|
'<span>💬 ' + (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 %}
|