// ============================================ // India20Sixty Dashboard v3.0 // Frontend for API Worker // ============================================ // CONFIG - Update this to your API worker URL const API_BASE = 'https://api.india20sixty.workers.dev'; // Constants const CATS = { AI: { label: 'AI & ML', color: '#00e5ff', emoji: '๐Ÿค–' }, Space: { label: 'Space & Defence', color: '#b388ff', emoji: '๐Ÿš€' }, Gadgets: { label: 'Gadgets & Tech', color: '#ffd740', emoji: '๐Ÿ“ฑ' }, DeepTech: { label: 'Deep Tech', color: '#ff6b35', emoji: '๐Ÿ”ฌ' }, GreenTech: { label: 'Green & Energy', color: '#00e676', emoji: 'โšก' }, Startups: { label: 'Startups', color: '#ff6b9d', emoji: '๐Ÿ’ก' } }; const VPD_SCHEDULES = { 1: ['12:00 PM IST'], 2: ['6:00 AM IST', '6:00 PM IST'], 3: ['6:00 AM IST', '12:00 PM IST', '6:00 PM IST'] }; const PROG = { pending: 8, processing: 35, images: 55, voice: 68, render: 82, upload: 93, staged: 20, mixing: 90, complete: 100, test_complete: 100, failed: 0 }; const PCOL = { pending: '#ffd740', processing: '#00e5ff', images: '#b388ff', voice: '#b388ff', render: '#ff6b35', upload: '#00e5ff', staged: '#ffd740', mixing: '#ff6b35', complete: '#00e676', test_complete: '#00e676', failed: '#ff5252' }; const BLBL = { pending: 'Pending', processing: 'Processing', images: 'Images', voice: 'Voice', render: 'Rendering', upload: 'Uploading', staged: 'Staged', mixing: 'Mixing', complete: 'Complete', test_complete: 'Complete', failed: 'Failed' }; const CHAR = { natural: 'No pitch shift', woman: '+4 st, formant +1.2', man: '-2 st deeper', elder: '-4 st, formant -0.8', child: '+9 st, formant +2.0', radio: 'Heavy compression + reverb' }; // State let activeTab = 'all'; let allJobs = []; let allTopics = []; let allAnalytics = []; let analyticsJobs = []; let topicFilter = 'ready'; let currentPage = 'home'; let activeCat = 'all'; let topicCat = 'all'; let currentVoiceMode = 'ai'; let calDate = new Date(); let calEvents = []; let studioJob = null; let mediaRecorder = null; let audioChunks = []; let recordedBlob = null; let audioCtx = null; let analyserNode = null; let recTimer = null; let recSecs = 0; let selectedMusic = null; let selectedPreset = 'natural'; let playbackAudio = null; let isRecording = false; let currentStagingTab = 'staged'; let allCBDP = []; let allStaged = []; let selectedImages = []; let libTopicFilter = 'all'; let allImages = []; let R2_BASE_URL = ''; // ============================================ // UTILS // ============================================ function ago(iso) { const s = Math.floor((Date.now() - new Date(iso)) / 1000); if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s / 60) + 'm'; if (s < 86400) return Math.floor(s / 3600) + 'h'; return Math.floor(s / 86400) + 'd'; } function fmt(n) { if (!n) return '0'; if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return String(n); } function scClass(s) { return s >= 80 ? 'sc-hi' : s >= 60 ? 'sc-med' : 'sc-lo'; } function badge(st) { const s = st || 'unknown'; const dot = ['pending', 'processing'].includes(s) ? '' : ''; return `${dot}${BLBL[s] || s}`; } function showDebug(id, html) { const el = document.getElementById(id); if (el) el.innerHTML = `
${html}
`; } // ============================================ // API CALLS // ============================================ async function apiGet(endpoint) { const r = await fetch(`${API_BASE}${endpoint}`); if (!r.ok) throw new Error(`API ${r.status}: ${await r.text()}`); return r.json(); } async function apiPost(endpoint, body) { const r = await fetch(`${API_BASE}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!r.ok) throw new Error(`API ${r.status}: ${await r.text()}`); return r.json(); } // ============================================ // PAGE NAVIGATION // ============================================ function showPage(name, btn) { document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('page-' + name).classList.add('active'); btn.classList.add('active'); currentPage = name; if (name === 'staging') { loadStaging(); renderStagingGrid(); } if (name === 'review') { loadCBDPReview(); } if (name === 'library') { loadLibrary(); } if (name === 'calendar') { loadCalendar(); renderCalendar(); } if (name === 'analytics') { renderAnalytics(); } if (name === 'topics') { renderTopicsPage(); } } // ============================================ // CATEGORY STRIPS // ============================================ function buildCatStrips() { const s = document.getElementById('cat-strip'); const ts = document.getElementById('topic-cat-strip'); const mc = document.getElementById('modal-cats'); Object.keys(CATS).forEach(k => { const cat = CATS[k]; // Home filter const p = document.createElement('div'); p.className = 'cat-pill'; p.dataset.cat = k; p.innerHTML = `${cat.emoji} ${cat.label} 0`; p.onclick = () => filterByCat(k, p); s.appendChild(p); // Topics filter const p2 = p.cloneNode(true); p2.onclick = () => filterTopicsByCat(k, p2); ts.appendChild(p2); // Modal categories const d = document.createElement('div'); d.className = 'cat-check selected'; d.dataset.cat = k; d.innerHTML = `${cat.emoji}${cat.label}`; d.onclick = () => d.classList.toggle('selected'); mc.appendChild(d); }); } function filterByCat(cat, btn) { activeCat = cat; document.querySelectorAll('#cat-strip .cat-pill').forEach(p => { const isA = p.dataset.cat === cat; p.classList.toggle('active', isA); p.style.borderColor = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : ''; p.style.color = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : ''; }); renderJobs(); } function filterTopicsByCat(cat, btn) { topicCat = cat; document.querySelectorAll('#topic-cat-strip .cat-pill').forEach(p => { const isA = p.dataset.cat === cat; p.classList.toggle('active', isA); p.style.borderColor = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : ''; p.style.color = isA ? (cat !== 'all' ? CATS[cat].color : 'var(--accent)') : ''; }); renderTopicsPage(); } function switchTab(tab) { activeTab = tab; document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); renderJobs(); } function switchStagingTab(tab) { currentStagingTab = tab; document.querySelectorAll('#stab-staged, #stab-cbdp').forEach(t => { t.classList.toggle('active', t.id === 'stab-' + tab); }); document.getElementById('stab-panel-staged').style.display = tab === 'staged' ? 'block' : 'none'; document.getElementById('stab-panel-cbdp').style.display = tab === 'cbdp' ? 'block' : 'none'; if (tab === 'cbdp') loadCBDPGrid(); } // ============================================ // DATA LOADING // ============================================ async function loadConfig() { try { const cfg = await apiGet('/config'); R2_BASE_URL = cfg.r2_base_url || ''; } catch (e) { console.error('Config load failed:', e); } } async function loadJobs() { try { allJobs = await apiGet('/jobs'); const run = allJobs.filter(j => ['pending', 'processing', 'images', 'voice', 'render', 'upload', 'staged', 'mixing'].includes(j.status)); const ok = allJobs.filter(j => j.status === 'complete' || j.status === 'test_complete'); const fail = allJobs.filter(j => j.status === 'failed'); document.getElementById('s-total').textContent = allJobs.length; document.getElementById('s-running').textContent = run.length; document.getElementById('s-complete').textContent = ok.length; document.getElementById('s-failed').textContent = fail.length; document.getElementById('tc-all').textContent = allJobs.length; document.getElementById('tc-run').textContent = run.length; document.getElementById('tc-ok').textContent = ok.length; document.getElementById('tc-fail').textContent = fail.length; document.getElementById('last-ref').textContent = 'Updated ' + new Date().toLocaleTimeString(); renderJobs(); } catch (e) { console.error('loadJobs:', e); showDebug('debug-home', 'Failed to load jobs: ' + e.message + ''); } } async function loadQueue() { try { allTopics = await apiGet('/topics?used=false&min_score=70'); const ready = allTopics.filter(t => !t.used && t.council_score >= 70); document.getElementById('s-topics').textContent = ready.length; Object.keys(CATS).forEach(k => { const el = document.getElementById('cc-' + k); if (el) el.textContent = ready.filter(t => t.cluster === k).length; }); const el = document.getElementById('queue-list'); if (!ready.length) { el.innerHTML = '
๐Ÿ“ซNo topics. Click Replenish.
'; return; } el.innerHTML = ready.slice(0, 8).map(t => { const cat = CATS[t.cluster]; return `
${t.topic}
${t.council_score} ${cat ? `${cat.emoji} ${t.cluster}` : ''} ${t.source || '-'}
`; }).join(''); } catch (e) { console.error('loadQueue:', e); } } async function loadStaging() { try { allStaged = await apiGet('/staging'); const cnt = document.getElementById('stg-cnt'); const stc = document.getElementById('stc-staged'); if (cnt) { cnt.textContent = allStaged.length; cnt.style.display = allStaged.length ? 'inline-block' : 'none'; } if (stc) stc.textContent = allStaged.length; if (currentPage === 'staging') renderStagingGrid(); } catch (e) { console.error('loadStaging:', e); } } async function loadCBDP() { try { allCBDP = await apiGet('/cbdp'); const cnt = document.getElementById('stc-cbdp'); if (cnt) cnt.textContent = allCBDP.length; if (currentPage === 'staging' && currentStagingTab === 'cbdp') loadCBDPGrid(); } catch (e) { console.error('loadCBDP:', e); } } async function loadCBDPReview() { try { const data = await apiGet('/review'); const allReview = Array.isArray(data) ? data : []; const rc = document.getElementById('rev-cnt'); const rc2 = document.getElementById('cbdp-count'); if (rc) { rc.textContent = allReview.length; rc.style.display = allReview.length ? 'inline-block' : 'none'; } if (rc2) rc2.textContent = allReview.length; renderReviewGrid(allReview); } catch (e) { console.error('loadCBDPReview:', e); } } async function loadLibrary() { const el = document.getElementById('lib-grid'); if (el) el.innerHTML = '
โณ Loading images...
'; try { allImages = await apiGet('/image-library'); const lc = document.getElementById('lib-count'); if (lc) lc.textContent = allImages.length; buildLibFilter(); renderLibrary(); } catch (e) { console.error('loadLibrary:', e); if (el) el.innerHTML = `
โš  Failed to load: ${e.message}
`; } } async function loadCalendar() { try { calEvents = await apiGet('/calendar'); if (currentPage === 'calendar') renderCalendar(); } catch (e) { console.error('loadCalendar:', e); } } async function loadAnalytics() { try { const data = await apiGet('/analytics'); allAnalytics = data.analytics || []; analyticsJobs = data.jobs || []; if (currentPage === 'analytics') renderAnalytics(); } catch (e) { console.error('loadAnalytics:', e); } } async function loadSystemState() { try { const state = await apiGet('/system-state'); setPublishUI(state.publish === true); currentVoiceMode = state.voice_mode || 'ai'; setVoiceModeUI(currentVoiceMode); const vpd = state.videos_per_day || 1; document.querySelectorAll('.vpd-btn').forEach(b => { const isA = b.id === 'vpd-' + vpd; b.className = 'btn ' + (isA ? 'btn-primary' : 'btn-ghost') + ' vpd-btn'; }); document.getElementById('sched-times').textContent = (VPD_SCHEDULES[vpd] || []).join(' โ€ข '); document.getElementById('sched-desc').textContent = vpd + ' video' + (vpd > 1 ? 's' : '') + '/day'; } catch (e) { console.error('loadSystemState:', e); } } // ============================================ // RENDERING // ============================================ function filterJobs(jobs, tab) { let j = jobs; if (activeCat !== 'all') j = j.filter(x => x.cluster === activeCat); if (tab === 'running') return j.filter(x => ['pending', 'processing', 'images', 'voice', 'render', 'upload', 'staged', 'mixing'].includes(x.status)); if (tab === 'complete') return j.filter(x => x.status === 'complete' || x.status === 'test_complete'); if (tab === 'failed') return j.filter(x => x.status === 'failed'); return j; } function renderJobs() { const el = document.getElementById('job-list'); const jobs = filterJobs(allJobs, activeTab); if (!jobs.length) { el.innerHTML = '
๐Ÿ“ซNo jobs here.
'; return; } el.innerHTML = jobs.map(j => { const prog = PROG[j.status] || 0; const col = PCOL[j.status] || '#5a6278'; const cat = CATS[j.cluster]; const catBadge = cat ? `${cat.emoji} ${j.cluster}` : ''; const yt = j.youtube_id && j.youtube_id !== 'TEST_MODE' ? `โ–ถ Watch` : (j.youtube_id === 'TEST_MODE' ? 'test' : ''); const err = j.error ? `${j.error.slice(0, 35)}` : ''; return `
${j.topic || 'Untitled'}
${catBadge}${j.council_score ? `${j.council_score}` : ''}${err}${yt ? `${yt}` : ''}
${badge(j.status)}
${prog}%
${j.updated_at ? ago(j.updated_at) + ' ago' : '-'}
`; }).join(''); } function renderStagingGrid() { const el = document.getElementById('staged-grid'); if (!el) return; if (!allStaged.length) { el.innerHTML = `
๐ŸŽฌNo staged videos.
${currentVoiceMode === 'human' ? 'Create a video and it will appear here.' : 'Switch to Human Voice mode first.'}
`; return; } el.innerHTML = allStaged.map(j => { const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ŸŽฌ', label: j.cluster || '?' }; const scr = (j.script_package && j.script_package.text) || ''; const ageStr = j.created_at ? ago(j.created_at) + ' ago' : ''; return `
${j.topic || 'Untitled'}
${cat.emoji} ${cat.label} ${j.council_score || 0} ${ageStr}
${scr.slice(0, 110)}${scr.length > 110 ? 'โ€ฆ' : ''}
๐ŸŽค Needs Voice Open Studio โ†’
`; }).join(''); } function loadCBDPGrid() { const el = document.getElementById('staging-cbdp-grid'); if (!el) return; if (!allCBDP.length) { el.innerHTML = '
๐Ÿ‘No CBDP jobs.
'; return; } el.innerHTML = allCBDP.map(j => { const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ŸŽฌ', label: j.cluster || '?' }; const age = j.created_at ? ago(j.created_at) + ' ago' : ''; return `
${j.topic || 'Untitled'}
${cat.emoji} ${cat.label} ${age}
${j.error || 'Upload failed'}
`; }).join(''); } function renderReviewGrid(allReview) { const el = document.getElementById('cbdp-grid'); if (!el) return; if (!allReview || !allReview.length) { el.innerHTML = `
๐ŸŽฌNo videos in review queue.
Videos land here when PUBLISH is OFF, or when YouTube upload fails after a successful render.
`; return; } try { el.innerHTML = allReview.map(j => { const cat = CATS[j.cluster] || { color: 'var(--muted)', emoji: '๐ŸŽฌ', label: j.cluster || '?' }; const scr = (j.script_package && j.script_package.text) || ''; const title = (j.script_package && j.script_package.title) || j.topic || 'Untitled'; const age = j.updated_at ? ago(j.updated_at) + ' ago' : ''; const reason = j.review_reason || 'Ready for review'; const hasVideo = !!(j.video_r2_url && R2_BASE_URL); const videoUrl = hasVideo ? `${R2_BASE_URL}/${j.video_r2_url}` : ''; const statusColor = j.status === 'review' ? 'var(--accent)' : 'var(--yellow)'; const statusLabel = j.status === 'review' ? 'REVIEW' : 'CBDP'; return `
${title}
${cat.emoji} ${cat.label} ${j.council_score || 0} ${statusLabel} ${age}
${reason}
${hasVideo ? `
โ–ถ Open in new tab
` : `
๐ŸŽฌ No video file saved This job failed before R2 save โ€” reject and re-run
` }
${scr ? scr.slice(0, 140) + (scr.length > 140 ? 'โ€ฆ' : '') : 'No script saved'}
${hasVideo ? `` : `` }
`; }).join(''); } catch (err) { console.error('renderReviewGrid error:', err); el.innerHTML = `
โš Error: ${err.message}
`; } } function buildLibFilter() { const topics = [...new Set(allImages.map(i => i.topic || 'unknown'))].filter(Boolean); const el = document.getElementById('lib-filter'); if (!el) return; el.innerHTML = `
All (${allImages.length})
` + topics.slice(0, 12).map(t => { const count = allImages.filter(i => i.topic === t).length; return `
${t.slice(0, 25)} (${count})
`; }).join(''); } function filterLib(topic, el) { libTopicFilter = topic; document.querySelectorAll('#lib-filter .cat-pill').forEach(p => { p.style.borderColor = ''; p.style.color = ''; }); if (el) { el.style.borderColor = 'var(--accent)'; el.style.color = 'var(--accent)'; } renderLibrary(); } function renderLibrary() { const el = document.getElementById('lib-grid'); if (!el) return; const imgs = libTopicFilter === 'all' ? allImages : allImages.filter(i => i.topic === libTopicFilter); if (!imgs.length) { el.innerHTML = `
๐Ÿ–ผ No images yet.
Images are saved automatically when you create a video.
`; return; } el.innerHTML = imgs.map((img, idx) => { const sel = selectedImages.indexOf(img.url) > -1; const selIdx = selectedImages.indexOf(img.url); return `
${sel ? `
${selIdx + 1}
` : ''}
${img.topic.slice(0, 30)}
`; }).join(''); } function renderCalendar() { const el = document.getElementById('cal-grid'); if (!el) return; const y = calDate.getFullYear(), m = calDate.getMonth(); document.getElementById('cal-lbl').textContent = calDate.toLocaleDateString('en-IN', { month: 'long', year: 'numeric' }); const first = new Date(y, m, 1).getDay(); const days = new Date(y, m + 1, 0).getDate(); const today = new Date(); let html = ''; for (let i = 0; i < first; i++) html += '
'; for (let d = 1; d <= days; d++) { const isToday = today.getDate() === d && today.getMonth() === m && today.getFullYear() === y; const evts = calEvents.filter(e => { const ed = new Date(e.scheduled_at || e.created_at); return ed.getFullYear() === y && ed.getMonth() === m && ed.getDate() === d; }); const evHtml = evts.map(e => { const cat = CATS[e.cluster] || { color: 'var(--accent)' }; const t = e.scheduled_at ? new Date(e.scheduled_at).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : ''; return `
${t ? t + ' ' : ''}${(e.topic || '').slice(0, 14)}
`; }).join(''); html += `
${d}
${evHtml}
`; } el.innerHTML = html; } function renderAnalytics() { const rows = allAnalytics; if (!rows.length) { ['a-views', 'a-likes', 'a-comments', 'a-avg'].forEach(id => { const e = document.getElementById(id); if (e) e.textContent = '-'; }); document.getElementById('video-grid').innerHTML = '
๐Ÿ“Š No analytics yet.
'; document.getElementById('perf-list').innerHTML = ''; return; } document.getElementById('a-views').textContent = fmt(rows.reduce((s, r) => s + (r.youtube_views || 0), 0)); document.getElementById('a-likes').textContent = fmt(rows.reduce((s, r) => s + (r.youtube_likes || 0), 0)); document.getElementById('a-comments').textContent = fmt(rows.reduce((s, r) => s + (r.comment_count || 0), 0)); document.getElementById('a-avg').textContent = fmt(rows.length ? Math.round(rows.reduce((s, r) => s + (r.score || 0), 0) / rows.length) : 0); document.getElementById('a-count').textContent = rows.length + ' videos'; const sorted = [...rows].sort((a, b) => b.score - a.score); document.getElementById('video-grid').innerHTML = sorted.map(r => { const job = analyticsJobs.find(j => j.id === r.video_id) || {}; const hasYt = job.youtube_id && job.youtube_id !== 'TEST_MODE'; return `
๐ŸŽฌ
${job.topic || 'Unknown'}
๐Ÿ‘ ${fmt(r.youtube_views || 0)} โค ${fmt(r.youtube_likes || 0)}
${fmt(r.score || 0)}
${hasYt ? `โ–ถ Watch` : ''}
`; }).join(''); const perfRow = r => { const j = analyticsJobs.find(x => x.id === r.video_id) || {}; return `
${j.topic || '-'}
${fmt(r.youtube_views || 0)}
${fmt(r.youtube_likes || 0)}
${fmt(r.score || 0)}
`; }; document.getElementById('perf-list').innerHTML = sorted.slice(0, 5).map(perfRow).join('') || '
No data
'; } function renderTopicsPage() { let topics = allTopics; if (topicFilter === 'ready') topics = topics.filter(t => !t.used && t.council_score >= 70); if (topicFilter === 'used') topics = topics.filter(t => t.used); if (topicCat !== 'all') topics = topics.filter(t => t.cluster === topicCat); document.getElementById('topics-count').textContent = topics.length + ' topics'; const el = document.getElementById('topics-list'); if (!topics.length) { el.innerHTML = '
๐Ÿ“ซNo topics.
'; return; } el.innerHTML = topics.map(t => { const cat = CATS[t.cluster]; const canGen = !t.used && t.council_score >= 70; return `
${t.topic}
${t.council_score} ${t.used ? 'Used' : 'Ready'} ${cat ? `${cat.emoji} ${cat.label}` : ''} ${t.source || '-'}
${canGen ? `` : ''}
`; }).join(''); } // ============================================ // ACTIONS // ============================================ async function doCreateJob() { const btn = document.getElementById('bc'); btn.disabled = true; btn.textContent = 'Creating...'; try { const d = await apiPost('/run', {}); switchTab('running'); loadJobs(); loadQueue(); showDebug('debug-home', `Job created: ${d.topic}`); } catch (e) { showDebug('debug-home', `${e.message}`); } finally { btn.disabled = false; btn.innerHTML = 'โ–ถ Create Video'; } } async function doGenerateTopic() { const btn = document.getElementById('bg'); btn.disabled = true; btn.textContent = 'Generating...'; try { const topic = prompt('Topic idea:', ''); if (topic === null) { btn.disabled = false; btn.innerHTML = 'โœฆ Generate Topic'; return; } // This would call your topic council - for now just show message showDebug('debug-home', 'Topic generation triggered (implement topic council call)'); loadQueue(); } catch (e) { showDebug('debug-home', `${e.message}`); } finally { btn.disabled = false; btn.innerHTML = 'โœฆ Generate Topic'; } } async function doKillIncomplete() { const run = allJobs.filter(j => ['pending', 'processing', 'images', 'voice', 'render', 'upload'].includes(j.status)); if (!run.length) { showDebug('debug-home', 'No incomplete jobs.'); return; } if (!confirm(`Kill ${run.length} job(s)?`)) return; try { const d = await apiPost('/kill-incomplete', {}); showDebug('debug-home', `Killed ${d.killed}. Restored: ${d.topics_restored}`); setTimeout(() => { loadJobs(); loadQueue(); }, 600); } catch (e) { showDebug('debug-home', `${e.message}`); } } async function doRestoreFailed() { const f = allJobs.filter(j => j.status === 'failed'); if (!f.length) { showDebug('debug-home', 'No failed jobs.'); return; } if (!confirm(`Restore ${f.length} jobs?`)) return; try { const d = await apiPost('/restore-failed', {}); showDebug('debug-home', `Restored ${d.restored}.`); setTimeout(() => { loadJobs(); loadQueue(); }, 600); } catch (e) { showDebug('debug-home', `${e.message}`); } } async function doTestAPI() { const btn = document.getElementById('bt'); if (btn) { btn.disabled = true; btn.textContent = 'Testing...'; } try { const health = await apiGet('/health'); const config = await apiGet('/config'); showDebug('debug-home', `API: ${health.version}
R2: ${config.r2_base_url ? 'OK' : 'Not set'}`); } catch (e) { showDebug('debug-home', `API Error: ${e.message}`); } finally { if (btn) { btn.disabled = false; btn.innerHTML = 'โšก Test API'; } } } async function doSyncAnalytics() { try { await apiPost('/sync-analytics', {}); showDebug('debug-home', 'Sync started.'); setTimeout(loadAnalytics, 8000); } catch (e) { alert(e.message); } } async function setVPD(n) { try { await apiPost('/system-state', { videos_per_day: n }); loadSystemState(); showDebug('debug-home', `Schedule: ${n} video${n > 1 ? 's' : ''}/day`); } catch (e) { showDebug('debug-home', `${e.message}`); } } async function toggleVoiceMode() { const newMode = currentVoiceMode === 'ai' ? 'human' : 'ai'; try { const d = await apiPost('/system-state', { voice_mode: newMode }); currentVoiceMode = d.voice_mode || newMode; setVoiceModeUI(currentVoiceMode); showDebug('debug-home', currentVoiceMode === 'human' ? '๐ŸŽค Human Voice Mode ON' : '๐Ÿค– AI Voice Mode'); } catch (e) { showDebug('debug-home', `${e.message}`); } } function setVoiceModeUI(mode) { const h = mode === 'human'; document.getElementById('vm-tog').style.background = h ? 'var(--green)' : 'var(--accent)'; document.getElementById('vm-knob').style.transform = h ? 'translateX(16px)' : 'translateX(0)'; document.getElementById('vm-lbl').textContent = h ? '๐ŸŽค HUMAN VOICE' : '๐Ÿค– AI VOICE'; document.getElementById('vm-lbl').style.color = h ? 'var(--green)' : 'var(--accent)'; const banner = document.getElementById('staging-banner'); if (banner) { banner.style.display = h ? 'none' : 'block'; banner.innerHTML = '๐Ÿค– AI Voice Mode โ€” pipeline auto-completes. Switch to Human Voice to use staging.'; } } async function togglePublish() { const knb = document.getElementById('pub-knob'); const isOn = knb.style.transform === 'translateX(16px)'; const newState = !isOn; setPublishUI(newState); try { await apiPost('/system-state', { publish: newState }); } catch (e) { setPublishUI(isOn); alert('Failed: ' + e.message); } } function setPublishUI(on) { document.getElementById('pub-tog').style.background = on ? 'var(--green)' : 'var(--red)'; document.getElementById('pub-knob').style.transform = on ? 'translateX(16px)' : 'translateX(0)'; document.getElementById('pub-lbl').textContent = on ? 'PUBLISH ON' : 'PUBLISH OFF'; document.getElementById('pub-lbl').style.color = on ? 'var(--green)' : 'var(--red)'; } async function retryUpload(jobId, btn) { btn.disabled = true; btn.textContent = 'Retrying...'; try { await apiPost('/publish-job', { job_id: jobId }); btn.textContent = 'โœ“ Retrying!'; btn.style.background = 'var(--green)'; setTimeout(loadCBDP, 2000); } catch (e) { btn.textContent = 'Retry Failed'; btn.disabled = false; alert(e.message); } } async function publishCBDP(jobId, btn) { if (!confirm('Publish this video to YouTube now?')) return; btn.disabled = true; btn.textContent = 'โณ Publishing...'; try { await apiPost('/publish-job', { job_id: jobId }); btn.textContent = 'โœ“ Sent!'; btn.style.background = 'var(--green)'; setTimeout(() => { loadCBDPReview(); loadJobs(); }, 1500); } catch (e) { btn.textContent = '๐Ÿš€ Publish'; btn.disabled = false; alert('Publish failed: ' + e.message); } } async function rejectCBDP(jobId, btn) { if (!confirm('Reject this video? The topic will return to queue for reuse.')) return; btn.disabled = true; btn.textContent = 'โณ...'; try { await apiPost('/reject-job', { job_id: jobId }); loadCBDPReview(); loadQueue(); } catch (e) { btn.textContent = 'โœ• Reject'; btn.disabled = false; alert('Reject failed: ' + e.message); } } async function generateNow(topicId, btn) { if (!confirm('Generate a video from this topic right now?')) return; btn.disabled = true; btn.textContent = 'โณ Creating...'; try { const d = await apiPost('/run-topic', { topic_id: topicId }); btn.textContent = 'โœ“ Job created!'; btn.style.color = 'var(--green)'; showDebug('debug-home', `Video job created from topic: ${d.topic}`); setTimeout(() => { loadJobs(); loadQueue(); renderTopicsPage(); }, 800); } catch (e) { btn.textContent = 'โ–ถ Generate Now'; btn.disabled = false; alert('Failed: ' + e.message); } } // ============================================ // IMAGE LIBRARY // ============================================ async function uploadLibImages(input) { const files = Array.from(input.files); if (!files.length) return; const btn = input.parentElement; const orig = btn.innerHTML; btn.style.color = 'var(--yellow)'; const topic = prompt('Tag these images with a topic name (used for filtering):', 'uploaded') || 'uploaded'; let ok = 0, fail = 0; for (let i = 0; i < files.length; i++) { const f = files[i]; btn.innerHTML = `โณ ${f.name.slice(0, 20)}... (${i + 1}/${files.length})`; try { const r = await fetch(`${API_BASE}/upload-image?topic=${encodeURIComponent(topic)}&filename=${encodeURIComponent(f.name)}`, { method: 'POST', headers: { 'Content-Type': f.type || 'image/png' }, body: f }); const d = await r.json(); if (d.error) throw new Error(d.error); ok++; } catch (e) { console.error('Upload failed:', f.name, e); fail++; } } btn.innerHTML = orig; btn.style.color = ''; input.value = ''; const msg = `โœ“ Uploaded ${ok} image${ok !== 1 ? 's' : ''}${fail ? ` (โœ— ${fail} failed)` : ''}`; showDebug('debug-home', `${msg}`); loadLibrary(); } function toggleLibImage(el) { const url = el.dataset.imgurl; const idx = selectedImages.indexOf(url); if (idx > -1) { selectedImages.splice(idx, 1); } else { if (selectedImages.length >= 3) { alert('Select exactly 3 images. Deselect one first.'); return; } selectedImages.push(url); } document.getElementById('lib-sel-count').textContent = selectedImages.length + ' / 3 selected'; const btn = document.getElementById('lib-create-btn'); btn.disabled = selectedImages.length !== 3; btn.style.opacity = selectedImages.length === 3 ? '1' : '.4'; renderLibrary(); } async function createVideoFromLibrary() { if (selectedImages.length !== 3) { alert('Select exactly 3 images first.'); return; } const btn = document.getElementById('lib-create-btn'); btn.disabled = true; btn.textContent = 'โณ Creating...'; try { const d = await apiPost('/run-with-images', { image_urls: selectedImages }); selectedImages = []; renderLibrary(); document.getElementById('lib-sel-count').textContent = '0 / 3 selected'; btn.textContent = 'โœ“ Job created!'; btn.style.color = 'var(--green)'; showDebug('debug-home', 'Video job created from library images'); setTimeout(() => { loadJobs(); showPage('home', document.querySelector('.nav-btn')); }, 1200); } catch (e) { btn.textContent = 'โ–ถ Create Video'; btn.disabled = false; btn.style.opacity = '1'; alert('Failed: ' + e.message); } } // ============================================ // STUDIO // ============================================ async function openStudio(jobId) { studioJob = allStaged.find(j => j.id === jobId); if (!studioJob) return; document.getElementById('stu-title').textContent = studioJob.topic || 'Studio'; document.getElementById('stu-id').textContent = jobId; document.getElementById('stu-script').textContent = (studioJob.script_package && studioJob.script_package.text) || 'No script'; const vid = document.getElementById('stu-vid'); const videoUrl = studioJob.video_r2_url && R2_BASE_URL ? `${R2_BASE_URL}/${studioJob.video_r2_url}` : ''; if (videoUrl) { vid.src = videoUrl; vid.load(); vid.onerror = () => { vid.style.display = 'none'; document.getElementById('stu-vid-err').style.display = 'flex'; }; vid.oncanplay = () => { vid.style.display = ''; document.getElementById('stu-vid-err').style.display = 'none'; }; } else { vid.removeAttribute('src'); document.getElementById('stu-vid-err').style.display = 'flex'; } await loadMusicList(); resetRec(); document.getElementById('studio').classList.remove('hidden'); document.body.style.overflow = 'hidden'; } function closeStudio() { document.getElementById('studio').classList.add('hidden'); document.body.style.overflow = ''; stopRec(); if (playbackAudio) { playbackAudio.pause(); playbackAudio = null; } studioJob = null; } async function loadMusicList() { try { const d = await apiGet('/music-library'); const icons = { Epic: 'โšก', Hopeful: '๐ŸŒ…', Tech: '๐Ÿ’ป', Emotional: '๐Ÿ’ซ', Neutral: '๐ŸŽต' }; document.getElementById('music-list').innerHTML = d.tracks.map(t => `
${icons[t.category] || '๐ŸŽต'}
${t.label}
${t.category} ยท ${t.duration}s
${selectedMusic === t.id ? 'โœ“' : ''}
`).join(''); } catch (e) { document.getElementById('music-list').innerHTML = '
Music unavailable
'; } } function selectMusic(id) { selectedMusic = id; loadMusicList(); } function setChar(el, preset) { selectedPreset = preset; document.querySelectorAll('.char-btn').forEach(b => b.classList.remove('active')); el.classList.add('active'); document.getElementById('char-desc').textContent = CHAR[preset] || ''; } // ============================================ // RECORDER // ============================================ async function startRec() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, sampleRate: 44100 } }); audioCtx = new AudioContext({ sampleRate: 44100 }); const src = audioCtx.createMediaStreamSource(stream); analyserNode = audioCtx.createAnalyser(); analyserNode.fftSize = 2048; const hpf = audioCtx.createBiquadFilter(); hpf.type = 'highpass'; hpf.frequency.value = 80; const comp = audioCtx.createDynamicsCompressor(); comp.threshold.value = -24; comp.ratio.value = 4; comp.attack.value = 0.003; comp.release.value = 0.25; const lim = audioCtx.createDynamicsCompressor(); lim.threshold.value = -3; lim.ratio.value = 20; lim.attack.value = 0.001; lim.release.value = 0.1; src.connect(hpf); hpf.connect(comp); comp.connect(analyserNode); analyserNode.connect(lim); lim.connect(audioCtx.destination); drawWaveform(); audioChunks = []; mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' }); mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); }; mediaRecorder.onstop = () => { recordedBlob = new Blob(audioChunks, { type: 'audio/webm' }); document.getElementById('rec-ply').disabled = false; document.getElementById('rec-rst').disabled = false; document.getElementById('rec-status').textContent = `โœ“ Recorded (${Math.round(recordedBlob.size / 1024)}KB)`; document.getElementById('rec-status').className = 'rec-status'; clearInterval(recTimer); }; mediaRecorder.start(100); isRecording = true; recSecs = 0; recTimer = setInterval(() => { recSecs++; const m = Math.floor(recSecs / 60); const s = recSecs % 60; document.getElementById('rec-dur').textContent = m + ':' + (s < 10 ? '0' : '') + s; }, 1000); document.getElementById('rec-rec').disabled = true; document.getElementById('rec-stp').disabled = false; document.getElementById('rec-status').textContent = 'โ— RECORDING...'; document.getElementById('rec-status').className = 'rec-status recording'; } catch (e) { alert('Microphone error: ' + e.message); } } function stopRec() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(t => t.stop()); } isRecording = false; document.getElementById('rec-rec').disabled = false; document.getElementById('rec-stp').disabled = true; } function playRec() { if (!recordedBlob) return; if (playbackAudio) { playbackAudio.pause(); playbackAudio = null; document.getElementById('rec-ply').textContent = 'โ–ถ'; return; } playbackAudio = new Audio(URL.createObjectURL(recordedBlob)); playbackAudio.play(); document.getElementById('rec-ply').textContent = 'โธ'; playbackAudio.onended = () => { document.getElementById('rec-ply').textContent = 'โ–ถ'; playbackAudio = null; }; } function resetRec() { stopRec(); if (playbackAudio) { playbackAudio.pause(); playbackAudio = null; } audioChunks = []; recordedBlob = null; recSecs = 0; document.getElementById('rec-rec').disabled = false; document.getElementById('rec-stp').disabled = true; document.getElementById('rec-ply').disabled = true; document.getElementById('rec-rst').disabled = true; document.getElementById('rec-status').textContent = 'Ready'; document.getElementById('rec-status').className = 'rec-status'; document.getElementById('rec-dur').textContent = '0:00'; const c = document.getElementById('waveform'); if (c) { const ctx2 = c.getContext('2d'); ctx2.clearRect(0, 0, c.width, c.height); } } function drawWaveform() { if (!analyserNode) return; const canvas = document.getElementById('waveform'); const ctx2 = canvas.getContext('2d'); const W = canvas.width = canvas.offsetWidth; const H = canvas.height; const buf = new Uint8Array(analyserNode.frequencyBinCount); function draw() { if (!isRecording) return; requestAnimationFrame(draw); analyserNode.getByteTimeDomainData(buf); ctx2.fillStyle = 'rgba(13,19,32,0.4)'; ctx2.fillRect(0, 0, W, H); ctx2.lineWidth = 1.5; ctx2.strokeStyle = '#00e5ff'; ctx2.beginPath(); const step = W / buf.length; for (let i = 0; i < buf.length; i++) { const y = (buf[i] / 128.0) * (H / 2); i === 0 ? ctx2.moveTo(0, y) : ctx2.lineTo(i * step, y); } ctx2.stroke(); } draw(); } function previewMix() { const vid = document.getElementById('stu-vid'); if (vid && vid.src) { vid.currentTime = 0; vid.play().catch(e => console.warn('Preview play failed:', e)); } if (recordedBlob) { if (playbackAudio) { playbackAudio.pause(); playbackAudio = null; } playbackAudio = new Audio(URL.createObjectURL(recordedBlob)); playbackAudio.play(); } } // ============================================ // PUBLISH // ============================================ async function doPublish(publishAt) { if (!studioJob) { alert('No job open'); return; } if (!recordedBlob) { alert('Please record your voice first'); return; } const sEl = document.getElementById('pub-status'); const n = document.getElementById('pub-now'); const s = document.getElementById('pub-sch'); n.disabled = s.disabled = true; sEl.textContent = 'โณ Uploading voice...'; sEl.style.color = 'var(--yellow)'; try { // Upload voice const ur = await fetch(`${API_BASE}/upload-voice?job_id=${studioJob.id}`, { method: 'POST', body: recordedBlob, headers: { 'Content-Type': 'audio/webm' } }); if (!ur.ok) throw new Error('Upload failed: ' + ur.status); sEl.textContent = 'โณ Starting mix...'; // Start mix const mr = await apiPost('/mix', { job_id: studioJob.id, music_track: selectedMusic || 'neutral_01', music_volume: (parseInt(document.getElementById('mus-vol').value) || 8) / 100, publish_at: publishAt || null, voice_offset_ms: parseInt(document.getElementById('voice-off').value) || 0 }); sEl.textContent = 'โœ“ ' + (publishAt ? 'Scheduled!' : 'Publishing soon!'); sEl.style.color = 'var(--green)'; allStaged = allStaged.filter(j => j.id !== studioJob.id); renderStagingGrid(); setTimeout(closeStudio, 2000); } catch (e) { sEl.textContent = 'โœ— ' + e.message; sEl.style.color = 'var(--red)'; } finally { n.disabled = s.disabled = false; } } function publishNow() { doPublish(null); } function publishScheduled() { const dt = document.getElementById('pub-at').value; if (!dt) { alert('Pick a date/time first'); return; } doPublish(new Date(dt).toISOString()); } // ============================================ // CALENDAR NAV // ============================================ function calPrev() { calDate = new Date(calDate.getFullYear(), calDate.getMonth() - 1, 1); renderCalendar(); } function calNext() { calDate = new Date(calDate.getFullYear(), calDate.getMonth() + 1, 1); renderCalendar(); } function calToday() { calDate = new Date(); renderCalendar(); } // ============================================ // MODALS // ============================================ function openReplenishModal() { document.getElementById('rep-modal').classList.remove('hidden'); } function closeReplenishModal() { document.getElementById('rep-modal').classList.add('hidden'); } async function doReplenish() { const cats = Array.from(document.querySelectorAll('#modal-cats .cat-check.selected')).map(d => d.dataset.cat); const target = parseInt(document.getElementById('tgt-slider').value); closeReplenishModal(); showDebug('debug-home', `Replenishing [${cats.join(', ')}] target ${target}...`); try { // This would call your topic council showDebug('debug-home', 'Replenish triggered (implement topic council call)'); setTimeout(loadQueue, 5000); } catch (e) { showDebug('debug-home', `${e.message}`); } } function filterTopics(f) { topicFilter = f; ['all', 'ready', 'used'].forEach(k => { const b = document.getElementById('bt-' + k); if (b) b.className = 'btn ' + (k === f ? 'btn-primary' : 'btn-ghost'); }); renderTopicsPage(); } // ============================================ // INIT // ============================================ function init() { buildCatStrips(); loadConfig(); loadAll(); setInterval(() => { loadJobs(); loadQueue(); loadStaging(); loadCBDP(); loadCBDPReview(); if (currentPage === 'analytics') loadAnalytics(); if (currentPage === 'calendar') renderCalendar(); }, 6000); } function loadAll() { loadJobs(); loadQueue(); loadSystemState(); loadAnalytics(); loadCalendar(); loadStaging(); loadCBDP(); loadCBDPReview(); } // Start init();