← back to Studio Zero
public/index.html
561 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Studio Zero — 0 → monetized AI YouTube channel in 90 days</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230a0a0c'/%3E%3Ccircle cx='16' cy='16' r='8' fill='%23e07b2f'/%3E%3Cpath d='M13 12l7 4-7 4z' fill='%230a0a0c'/%3E%3C/svg%3E">
<style>
:root{
--bg:#0a0a0c; --panel:#121216; --panel2:#17171d; --line:#26262e;
--ink:#f4f4f6; --mut:#9a9aa6; --faint:#6a6a76;
--orange:#e07b2f; --orange2:#f0a03a; --red:#ff2b2b; --red2:#c62828;
--ok:#3ecf8e; --grad:linear-gradient(100deg,var(--orange) 0%,var(--orange2) 35%,var(--red) 100%);
--cols:3;
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
a{color:var(--orange2)}
.wrap{max-width:1080px;margin:0 auto;padding:0 20px 80px}
header.top{position:sticky;top:0;z-index:20;background:rgba(10,10,12,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.top .row{max-width:1080px;margin:0 auto;padding:12px 20px;display:flex;align-items:center;gap:14px}
.brand{font-weight:800;letter-spacing:.14em;font-size:15px;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
.brand .z{font-size:19px}
.spacer{flex:1}
.chip{font-size:12px;padding:4px 10px;border:1px solid var(--line);border-radius:999px;color:var(--mut);white-space:nowrap}
.chip b{color:var(--ink)}
.chip.mode-live{border-color:#2b5a3f;color:#8ee6b6}
.chip.mode-demo{border-color:#5a4a2b;color:#e6c98e}
.chip.cost b{color:var(--orange2)}
.hero{padding:60px 0 34px;text-align:center}
.kicker{font-size:12px;letter-spacing:.22em;color:var(--faint);text-transform:uppercase}
.hero h1{font-size:clamp(30px,5vw,52px);line-height:1.05;margin:14px 0 8px;font-weight:800;letter-spacing:-.02em}
.hero h1 .g{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
.hero p.sub{color:var(--mut);font-size:18px;max-width:640px;margin:0 auto 8px}
.hero .src{font-size:12px;color:var(--faint);margin-top:10px}
.cta{display:inline-flex;gap:10px;margin-top:26px}
.btn{cursor:pointer;border:0;border-radius:11px;padding:13px 22px;font-weight:700;font-size:15px;color:#fff;background:var(--grad);box-shadow:0 6px 22px rgba(224,60,40,.28);transition:transform .08s ease,filter .15s}
.btn:hover{filter:brightness(1.06)} .btn:active{transform:translateY(1px)}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink);box-shadow:none}
.btn.sm{padding:8px 14px;font-size:13px;border-radius:9px}
.btn:disabled{opacity:.5;cursor:not-allowed}
.saved{margin:34px auto 0;max-width:720px;text-align:left}
.saved h3{font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin:0 0 12px}
.pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
.pcard{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;cursor:pointer;transition:border-color .15s}
.pcard:hover{border-color:var(--orange)}
.pcard .pn{font-weight:700}
.pcard .pmeta{font-size:12px;color:var(--mut);margin-top:4px}
.pcard .when{font-size:11px;color:var(--faint);margin-top:8px;display:flex;align-items:center;gap:5px}
.pcard .prog{height:4px;border-radius:999px;background:var(--line);margin-top:10px;overflow:hidden}
.pcard .prog i{display:block;height:100%;background:var(--grad)}
.pcard .del{float:right;color:var(--faint);font-size:12px}
.pcard .del:hover{color:var(--red)}
.builderbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:22px 0 2px}
.builderbar .pname{font-weight:700;font-size:17px;background:none;border:0;border-bottom:1px solid transparent;color:var(--ink);padding:2px 0;min-width:120px;max-width:340px}
.builderbar .pname:hover{border-bottom-color:var(--line)} .builderbar .pname:focus{outline:0;border-bottom-color:var(--orange)}
.builderbar .prog2{font-size:12px;color:var(--faint)}
.builderbar .sp{flex:1}
.builderbar .done0{opacity:.45;pointer-events:none}
.stepper{display:flex;align-items:center;gap:0;margin:16px 0 26px;overflow-x:auto;padding-bottom:6px}
.snode{display:flex;align-items:center;flex:0 0 auto}
.sdot{width:30px;height:30px;border-radius:50%;border:2px solid var(--line);display:grid;place-items:center;font-size:13px;font-weight:700;color:var(--mut);background:var(--panel);cursor:pointer;transition:.15s}
.sdot.done{background:var(--grad);border-color:transparent;color:#fff}
.sdot.active{border-color:var(--orange);color:var(--orange2);box-shadow:0 0 0 4px rgba(224,123,47,.15)}
.sline{width:34px;height:2px;background:var(--line)}
.sline.done{background:var(--grad)}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:26px}
.steptag{font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--orange2);font-weight:700}
.panel h2{font-size:26px;margin:8px 0 4px;letter-spacing:-.01em}
.panel .blurb{color:var(--mut);margin:0 0 4px}
.gives{font-size:13px;color:var(--faint);border-left:2px solid var(--orange);padding-left:10px;margin:14px 0 18px}
.promptbox{background:#0d0d10;border:1px solid var(--line);border-radius:10px;padding:14px 16px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;color:#cfcfd6;white-space:pre-wrap;position:relative}
.promptbox .tok{color:var(--orange2);font-weight:700}
.promptbox .lbl{position:absolute;top:-9px;left:12px;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);background:var(--panel);padding:0 6px}
.fields{display:grid;gap:12px;margin:18px 0}
.field label{display:block;font-size:12px;color:var(--mut);margin-bottom:5px;letter-spacing:.02em}
.field input,.field textarea{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;color:var(--ink);padding:11px 12px;font:14px/1.5 inherit}
.field textarea{min-height:120px;resize:vertical;font-family:ui-monospace,Menlo,monospace;font-size:13px}
.field input:focus,.field textarea:focus{outline:0;border-color:var(--orange)}
.genrow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:6px}
.estcost{font-size:12px;color:var(--faint)}
.estcost b{color:var(--orange2)}
.out{margin-top:22px;border-top:1px dashed var(--line);padding-top:20px;display:none}
.out.show{display:block}
.out .ohead{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.out .ohead .t{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
.out .badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--mut)}
.out .badge.demo{border-color:#5a4a2b;color:#e6c98e}
.out .badge.live{border-color:#2b5a3f;color:#8ee6b6}
.out .badge.fallback{border-color:var(--red2);color:#ff9a9a}
.fbanner{display:none;margin:14px 0 4px;padding:11px 14px;border:1px solid var(--red2);border-left:3px solid var(--red);border-radius:8px;background:rgba(198,40,40,.10);color:#ffb3b3;font-size:13px;line-height:1.45}
.fbanner.show{display:block}
.fbanner b{color:#ff8a8a}
.fbanner .rs{color:var(--mut);font-size:12px}
.md{color:#e9e9ee}
.md h3{font-size:17px;margin:16px 0 8px} .md h4{font-size:15px;margin:14px 0 6px;color:var(--orange2)}
.md p{margin:8px 0} .md ul,.md ol{margin:8px 0;padding-left:22px} .md li{margin:3px 0}
.md code{background:#0d0d10;border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:12px}
.md strong{color:#fff}
.md table{border-collapse:collapse;width:100%;margin:12px 0;font-size:13px}
.md th,.md td{border:1px solid var(--line);padding:7px 10px;text-align:left}
.md th{background:var(--panel2);color:var(--orange2)}
.spin{width:16px;height:16px;border:2px solid var(--line);border-top-color:var(--orange);border-radius:50%;animation:sp .7s linear infinite;display:inline-block;vertical-align:-3px}
@keyframes sp{to{transform:rotate(360deg)}}
.calbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:8px 0 14px}
.calbar label{font-size:12px;color:var(--mut);display:flex;align-items:center;gap:7px}
.calbar select,.calbar input[type=range]{accent-color:var(--orange)}
.calbar select{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:6px 8px;font:13px inherit}
.calgrid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:10px}
.vcard{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:12px;display:flex;gap:10px;align-items:flex-start}
.vcard .num{font-weight:800;color:var(--faint);font-size:13px}
.vcard .vt{font-size:13px;line-height:1.4}
.navrow{display:flex;justify-content:space-between;margin-top:24px;gap:12px}
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 16px;font-size:13px;opacity:0;pointer-events:none;transition:opacity .2s;z-index:50}
.toast.show{opacity:1}
.footnote{text-align:center;color:var(--faint);font-size:12px;margin-top:40px}
.hidden{display:none!important}
</style>
</head>
<body>
<header class="top">
<div class="row">
<div class="brand">STUDIO <span class="z">ZERO</span></div>
<span id="modeChip" class="chip">…</span>
<div class="spacer"></div>
<span id="costChip" class="chip cost">session <b>$0.0000</b></span>
<button class="btn ghost sm" id="homeBtn">+ New</button>
</div>
</header>
<div class="wrap">
<section id="home">
<div class="hero">
<div class="kicker">7 Claude prompts · one guided workflow</div>
<h1>0 → <span class="g">monetized</span> AI YouTube channel<br>in 90 days.</h1>
<p class="sub">Studio Zero runs the seven prompts a $10K/mo creator agency would — and hands you the actual deliverables: niche, brand, a full quarter of content, scripts, thumbnails, and a money map.</p>
<div class="cta">
<button class="btn" id="startBtn">Start my channel →</button>
<button class="btn ghost" id="autoHeroBtn">⚡ Auto-build all 7</button>
<button class="btn ghost" id="demoBtn">See a demo run</button>
</div>
<div class="src">Method seeded by <a href="https://x.com/David263146/status/2079054195941147042" target="_blank" rel="noopener noreferrer">@David263146</a> · prompts run verbatim.</div>
</div>
<div class="saved" id="savedWrap" style="display:none">
<h3>Your channels</h3>
<div class="pgrid" id="savedGrid"></div>
</div>
</section>
<section id="builder" class="hidden">
<div class="builderbar" id="builderbar"></div>
<div class="stepper" id="stepper"></div>
<div class="panel" id="panel"></div>
<div class="footnote">Deliverables autosave to this browser · nothing is published. Studio Zero — accelerator prototype.</div>
</section>
</div>
<div class="toast" id="toast"></div>
<script>
const $=(s,r=document)=>r.querySelector(s), $$=(s,r=document)=>[...r.querySelectorAll(s)];
let CFG={liveMode:false,model:'demo',rate:{in:3,out:15}}, PROMPTS=[], STATE=null, cur=0, sessionCost=0, autoMsg='';
const LS_PROJECTS='sz_projects', LS_CAL='sz_cal_prefs';
function toast(m){const t=$('#toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1800);}
function copyText(text,okMsg){
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(text).then(()=>toast(okMsg),()=>toast('Copy failed — clipboard blocked'));
else toast('Copy not supported here');
}
function fmtCost(n){return '$'+Number(n||0).toFixed(4);}
function fmtWhen(iso){try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return iso;}}
function uid(){return 'ch_'+Math.random().toString(36).slice(2,9);}
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
function mdInline(s){return esc(s).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>').replace(/`(.+?)`/g,'<code>$1</code>').replace(/\[(.+?)\]\((https?:[^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');}
function md(src){
const lines=String(src).replace(/\r/g,'').split('\n');let html='',i=0;
while(i<lines.length){
let l=lines[i];
if(/^\s*\|.*\|\s*$/.test(l)&&i+1<lines.length&&/^\s*\|[-: |]+\|\s*$/.test(lines[i+1])){
const rows=[];while(i<lines.length&&/^\s*\|.*\|\s*$/.test(lines[i])){rows.push(lines[i]);i++;}
const cells=r=>r.trim().replace(/^\||\|$/g,'').split('|').map(c=>c.trim());
const head=cells(rows[0]);let t='<table><thead><tr>'+head.map(h=>'<th>'+mdInline(h)+'</th>').join('')+'</tr></thead><tbody>';
for(let r=2;r<rows.length;r++){t+='<tr>'+cells(rows[r]).map(c=>'<td>'+mdInline(c)+'</td>').join('')+'</tr>';}
html+=t+'</tbody></table>';continue;
}
let m;
if(m=l.match(/^\s*####\s+(.*)/)){html+='<h4>'+mdInline(m[1])+'</h4>';i++;continue;}
if(m=l.match(/^\s*###\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
if(m=l.match(/^\s*##\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
if(m=l.match(/^\s*#\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
if(/^\s*[-*]\s+/.test(l)){let it='';while(i<lines.length&&/^\s*[-*]\s+/.test(lines[i])){it+='<li>'+mdInline(lines[i].replace(/^\s*[-*]\s+/,''))+'</li>';i++;}html+='<ul>'+it+'</ul>';continue;}
if(/^\s*\d+\.\s+/.test(l)){let it='';while(i<lines.length&&/^\s*\d+\.\s+/.test(lines[i])){it+='<li>'+mdInline(lines[i].replace(/^\s*\d+\.\s+/,''))+'</li>';i++;}html+='<ol>'+it+'</ol>';continue;}
if(l.trim()===''){i++;continue;}
html+='<p>'+mdInline(l)+'</p>';i++;
}
return html;
}
function loadProjects(){try{return JSON.parse(localStorage.getItem(LS_PROJECTS)||'[]');}catch(e){return [];}}
function saveProjects(a){localStorage.setItem(LS_PROJECTS,JSON.stringify(a));}
function persist(){
if(!STATE)return;
const all=loadProjects();const idx=all.findIndex(p=>p.id===STATE.id);
STATE.updatedAt=new Date().toISOString();
if(idx>=0)all[idx]=STATE;else all.push(STATE);
saveProjects(all);
}
function newProject(){return {id:uid(),name:'Untitled channel',createdAt:new Date().toISOString(),niche:'',outputs:{},costs:{}};}
// ---- builder toolbar: rename + export ----
function stepByKey(k){return PROMPTS.find(s=>s.key===k);}
function planMarkdown(){
const lines=[];
lines.push('# '+(STATE.name||'Untitled channel'));
lines.push('');
lines.push('*0 → monetized AI YouTube channel in 90 days — built with Studio Zero.*');
if(STATE.niche)lines.push('','**Niche:** '+STATE.niche);
lines.push('','_Generated '+new Date().toLocaleString()+'_','');
PROMPTS.forEach(s=>{
const o=STATE.outputs[s.key];if(!o)return;
lines.push('','---','','## Step '+s.id+' · '+s.title,'');
lines.push(o.text.trim(),'');
});
const done=Object.keys(STATE.outputs||{}).length;
if(!done)lines.push('','_(No steps generated yet.)_');
return lines.join('\n');
}
function planHTML(){
const body=md(planMarkdown());
return '<!doctype html><html><head><meta charset="utf-8"><title>'+escAttr(STATE.name||'Channel plan')+' — Studio Zero</title>'
+'<style>body{max-width:760px;margin:40px auto;padding:0 20px;font:16px/1.6 -apple-system,BlinkMacSystemFont,Segoe UI,Inter,system-ui,sans-serif;color:#1a1a1f}'
+'h1{font-size:30px;letter-spacing:-.02em}h2{font-size:21px;margin-top:32px;border-top:1px solid #eee;padding-top:20px}h3{font-size:17px}h4{font-size:15px;color:#c0501a}'
+'table{border-collapse:collapse;width:100%;margin:14px 0;font-size:14px}th,td{border:1px solid #ddd;padding:7px 10px;text-align:left}th{background:#faf6f2}'
+'code{background:#f4f4f6;border-radius:4px;padding:1px 5px;font-size:13px}hr{display:none}a{color:#c0501a}'
+'@media print{body{margin:0}}</style></head><body>'+body
+'<p style="margin-top:40px;color:#999;font-size:12px">Studio Zero — prompts seeded by @David263146.</p></body></html>';
}
function escAttr(s){return String(s).replace(/"/g,'"').replace(/</g,'<');}
function slugify(s){return String(s||'channel').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,40)||'channel';}
function downloadFile(name,content,mime){
const b=new Blob([content],{type:mime});const u=URL.createObjectURL(b);
const a=document.createElement('a');a.href=u;a.download=name;document.body.appendChild(a);a.click();a.remove();
setTimeout(()=>URL.revokeObjectURL(u),2000);
}
function renderBar(){
const done=Object.keys(STATE.outputs||{}).length;
const running=!!autoMsg;
const bar=$('#builderbar');
bar.innerHTML=`
<input class="pname" id="pname" value="${escAttr(STATE.name||'Untitled channel')}" title="Rename this channel" ${running?'disabled':''}>
<span class="prog2" id="autostatus">${running?'<span class="spin"></span> '+esc(autoMsg):done+'/7 steps'}</span>
<span class="sp"></span>
<button class="btn ghost sm" id="autoBtn" ${running?'disabled':''}>⚡ Auto-build all 7</button>
${done&&!running?'<button class="btn ghost sm" id="dlBtn">⤓ Download plan</button><button class="btn ghost sm" id="mdBtn">⧉ Copy markdown</button>':''}`;
$('#pname').onchange=()=>{STATE.name=$('#pname').value.trim()||'Untitled channel';persist();};
$('#autoBtn').onclick=()=>autoBuildAll();
if(done&&!running){
$('#dlBtn').onclick=()=>{ downloadFile('studio-zero-'+slugify(STATE.name)+'.html',planHTML(),'text/html'); toast('Downloaded your channel plan'); };
$('#mdBtn').onclick=()=>{ copyText(planMarkdown(),'Copied full plan as Markdown'); };
}
}
function setAuto(m){ autoMsg=m; renderBar(); }
// Generate a single step from an explicit value map (DOM-independent — used by auto-build).
async function generateInto(step,vals){
const prompt=fillPrompt(step,vals);
const res=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,stepKey:step.key})});
const j=await res.json();
if(vals['[NICHE]'])STATE.niche=vals['[NICHE]'];
STATE.outputs[step.key]={text:j.text,demo:j.demo,fallback:!!j.fallback,warning:j.warning||null,cost:j.cost||0,maxPlan:!!j.maxPlan,equiv:j.equiv||null,at:new Date().toISOString()};
STATE.costs[step.key]=j.cost||0;
if(j.cost){sessionCost+=j.cost;updateCostChip();}
persist();
return j;
}
// One-click auto-pilot: chains all 7 steps, deriving cross-step inputs (niche → title → script).
async function autoBuildAll(){
if(autoMsg)return; // already running
let niche=STATE.niche;
if(!niche){ niche=(window.prompt('What is your channel about? (your niche)','')||'').trim(); if(!niche){ toast('Need a niche to auto-build'); return; } STATE.niche=niche; }
if(!STATE.name||STATE.name==='Untitled channel')STATE.name=niche.slice(0,42);
let autoTitle='', script='';
for(let i=0;i<PROMPTS.length;i++){
const s=PROMPTS[i]; cur=i;
setAuto('Generating step '+s.id+'/7 · '+s.title+'…'); renderStepper();
const vals={};
(s.inputs||[]).forEach(inp=>{
const t=inp.token;
if(t==='[NICHE]')vals[t]=niche;
else if(t==='[VIDEO TITLE]'||t==='[TITLE]')vals[t]=autoTitle||('The Complete '+niche+' Guide');
else if(t==='[TONE]')vals[t]=inp.default||'conversational';
else if(t==='[X]')vals[t]=inp.default||'0';
else if(t==='[SCRIPT]')vals[t]=(script||'').slice(0,6000)||('A video about '+niche);
else vals[t]=inp.default||'';
});
try{
const j=await generateInto(s,vals);
if(s.key==='roadmap'){ const ts=parseTitles(j.text); if(ts.length)autoTitle=ts[0]; }
if(s.key==='script'){ script=j.text; }
}catch(e){ toast('Step '+s.id+' failed — continuing'); }
renderStepper();
}
setAuto('');
cur=6; renderStep(); renderStepper(); renderBar();
toast('Auto-build complete — 7/7 · download your plan');
}
function renderStepper(){
const el=$('#stepper');el.innerHTML='';
PROMPTS.forEach((s,idx)=>{
const done=!!(STATE&&STATE.outputs[s.key]);
const node=document.createElement('div');node.className='snode';
const dot=document.createElement('div');
dot.className='sdot'+(idx===cur?' active':'')+(done?' done':'');
dot.textContent=done&&idx!==cur?'✓':s.id;dot.title=s.title;
dot.onclick=()=>{cur=idx;renderStep();renderStepper();};
node.appendChild(dot);
if(idx<PROMPTS.length-1){const ln=document.createElement('div');ln.className='sline'+(done?' done':'');node.appendChild(ln);}
el.appendChild(node);
});
}
function fillPrompt(step,vals){
let p=step.prompt;
(step.inputs||[]).forEach(inp=>{p=p.split(inp.token).join(vals[inp.token]||inp.token);});
return p;
}
function promptHTML(step,vals){
let p=esc(step.prompt);
(step.inputs||[]).forEach(inp=>{
const v=vals[inp.token];
const rep=v?('<span class="tok">'+esc(v)+'</span>'):('<span class="tok">'+esc(inp.token)+'</span>');
p=p.split(esc(inp.token)).join(rep);
});
return p;
}
function fid(token){return 'f_'+token.replace(/[^a-z]/gi,'');}
function currentVals(step){
const vals={};
(step.inputs||[]).forEach(inp=>{
const f=$('#'+fid(inp.token));
let v=f?f.value.trim():'';
if(!v&&inp.carriesFrom==='niche'&&STATE.niche)v=STATE.niche;
if(!v&&inp.default)v=inp.default;
vals[inp.token]=v;
});
return vals;
}
function renderStep(){
const step=PROMPTS[cur];if(!step)return;
const saved=STATE.outputs[step.key];
const prefill={};
(step.inputs||[]).forEach(inp=>{if(inp.carriesFrom==='niche'&&STATE.niche)prefill[inp.token]=STATE.niche;if(inp.default&&!prefill[inp.token])prefill[inp.token]=inp.default;});
const p=$('#panel');
p.innerHTML=`
<div class="steptag">Step ${step.id} of 7 · ${esc(step.title)}</div>
<h2>${esc(step.title)}</h2>
<p class="blurb">${esc(step.blurb)}</p>
<div class="gives"><b>What this gives you:</b> ${esc(step.gives)}</div>
<div class="promptbox"><span class="lbl">the prompt · run verbatim</span><span id="pbtext">${promptHTML(step,prefill)}</span></div>
<div class="fields" id="fields"></div>
<div class="genrow">
<button class="btn" id="genBtn">${CFG.liveMode?'Generate with Claude':'Generate (demo)'} →</button>
<span class="estcost" id="estCost"></span>
</div>
<div class="out ${saved?'show':''}" id="out">
<div class="ohead"><span class="t">Deliverable</span><span class="badge" id="outBadge"></span><span style="flex:1"></span><button class="btn ghost sm" id="copyBtn">Copy</button></div>
<div class="fbanner" id="fbanner"></div>
<div id="calWrap"></div>
<div class="md" id="mdOut"></div>
</div>
<div class="navrow">
<button class="btn ghost" id="prevBtn" ${cur===0?'disabled':''}>← Back</button>
<button class="btn ghost" id="nextBtn" ${cur===6?'disabled':''}>Next step →</button>
</div>`;
const ff=$('#fields');
(step.inputs||[]).forEach(inp=>{
const id=fid(inp.token);
const val=prefill[inp.token]||'';
const wrap=document.createElement('div');wrap.className='field';
wrap.innerHTML=`<label>${esc(inp.label)} <span style="color:var(--faint)">${esc(inp.token)}</span></label>`+
(inp.textarea?`<textarea id="${id}" placeholder="${esc(inp.placeholder||'')}"></textarea>`:`<input id="${id}" value="${esc(val)}" placeholder="${esc(inp.placeholder||'')}">`);
ff.appendChild(wrap);
const fld=$('#'+id);
fld.addEventListener('input',()=>{$('#pbtext').innerHTML=promptHTML(step,currentVals(step));updateEst();});
});
updateEst();
if(saved){renderOutput(step,saved.text,saved.demo,saved.cost,saved.maxPlan,saved.equiv,saved.fallback,saved.warning);}
$('#genBtn').onclick=doGenerate;
$('#prevBtn').onclick=()=>{if(cur>0){cur--;renderStep();renderStepper();}};
$('#nextBtn').onclick=()=>{if(cur<6){cur++;renderStep();renderStepper();}};
$('#copyBtn').onclick=()=>{copyText(STATE.outputs[step.key].text,'Copied deliverable');};
}
function updateEst(){
const step=PROMPTS[cur];const p=fillPrompt(step,currentVals(step));
const est=((p.length/4)/1e6*CFG.rate.in)+(1600/1e6*CFG.rate.out);
const e=$('#estCost');if(!e)return;
if(CFG.maxPlan)e.innerHTML='Max plan · <b>$0 marginal</b> <span style="color:var(--faint)">(covered by subscription)</span>';
else if(CFG.liveMode)e.innerHTML=`est. <b>${fmtCost(est)}</b> · ${esc(CFG.model)}`;
else e.innerHTML='demo mode · <b>$0 (local)</b>';
}
async function doGenerate(){
const step=PROMPTS[cur];const vals=currentVals(step);
for(const inp of (step.inputs||[])){if(!vals[inp.token]){toast('Fill in '+inp.label);return;}}
const prompt=fillPrompt(step,vals);
const btn=$('#genBtn');btn.disabled=true;btn.innerHTML='<span class="spin"></span> Generating…';
$('#out').classList.add('show');$('#mdOut').innerHTML='<p style="color:var(--mut)">Claude is working…</p>';$('#calWrap').innerHTML='';
try{
const res=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,stepKey:step.key})});
const j=await res.json();
const nicheVal=vals['[NICHE]'];if(nicheVal)STATE.niche=nicheVal;
if(STATE.name==='Untitled channel'&&STATE.niche)STATE.name=STATE.niche.slice(0,42);
STATE.outputs[step.key]={text:j.text,demo:j.demo,fallback:!!j.fallback,warning:j.warning||null,cost:j.cost||0,maxPlan:!!j.maxPlan,equiv:j.equiv||null,at:new Date().toISOString()};
STATE.costs[step.key]=j.cost||0;
if(j.cost){sessionCost+=j.cost;updateCostChip();}
if(j.fallback)toast('⚠ Live generation failed — showing placeholder demo');
persist();
renderOutput(step,j.text,j.demo,j.cost,j.maxPlan,j.equiv,j.fallback,j.warning);
renderStepper();renderBar();
}catch(e){
$('#mdOut').innerHTML='<p style="color:var(--red2)">Error: '+esc(e.message)+'</p>';
}finally{
btn.disabled=false;btn.innerHTML=(CFG.liveMode?'Regenerate':'Regenerate (demo)')+' ↻';
}
}
function renderOutput(step,text,demo,cost,maxPlan,equiv,fallback,warn){
$('#out').classList.add('show');
const b=$('#outBadge');
if(fallback)b.textContent='fallback demo';
else if(demo)b.textContent='demo';
else if(maxPlan)b.textContent='Max plan · $0'+(equiv?' (≈'+fmtCost(equiv)+' metered)':'');
else b.textContent=CFG.model+' · '+fmtCost(cost||0);
b.className='badge '+(fallback?'fallback':demo?'demo':'live');
// Persistent honesty banner: whenever a LIVE call failed and we substituted canned
// demo, say so plainly and keep it on screen — never let fake content read as real.
const fb=$('#fbanner');
if(fb){
if(fallback){
const rs=warn?String(warn).replace(/^live call failed:\s*/,''):'';
fb.innerHTML='<b>⚠ Generation unavailable.</b> The live provider call failed'+
(rs?' <span class="rs">('+esc(rs)+')</span>':'')+
' — this is <b>placeholder demo content</b>, not a real result. Check the provider token/connection and regenerate.';
fb.classList.add('show');
}else{fb.classList.remove('show');fb.innerHTML='';}
}
if(step.renderCalendar){renderCalendar(text);}else{$('#calWrap').innerHTML='';}
$('#mdOut').innerHTML=md(text);
}
function parseTitles(text){
const lines=String(text).split('\n');
const titles=[], seen=new Set();
const META=/^(type|why|seo|format|priority|goal|upload|video|phase|cadence|length|strategic|target|audience|hook|cta)\b/i;
const push=raw=>{
let t=String(raw).replace(/\*\*/g,'').replace(/^\s*\d+[\.\)]\s*/,'').replace(/^[\s"'“”‘’]+|[\s"'“”‘’:]+$/g,'').replace(/\s+/g,' ').trim();
const key=t.toLowerCase();
if(t.length>=8 && t.length<=160 && !META.test(t) && !seen.has(key)){ seen.add(key); titles.push(t); }
};
// Pass A — whole-line bold titles (the live format: **"Title"**, optionally number-prefixed)
for(const l of lines){ const m=l.match(/^\s*(?:\d+[\.\)]\s*)?\*\*(.+?)\*\*\s*$/); if(m) push(m[1]); }
// Pass B — numbered plain lines (demo format: "1. Title")
if(titles.length<12){ for(const l of lines){ const m=l.match(/^\s*\d+[\.\)]\s+(.+?)\s*$/); if(m) push(m[1]); } }
// Pass C — quoted titles anywhere (last-resort fallback)
if(titles.length<6){ for(const l of lines){ const m=l.match(/[“"]([^”"]{15,140})[”"]/); if(m) push(m[1]); } }
return titles.slice(0,12);
}
function renderCalendar(text){
const titles=parseTitles(text);const wrap=$('#calWrap');
if(titles.length<3){wrap.innerHTML='';return;}
let prefs={};try{prefs=JSON.parse(localStorage.getItem(LS_CAL)||'{}');}catch(e){}
const sort=prefs.sort||'newest', cols=prefs.cols||3;
wrap.innerHTML=`
<div class="calbar">
<label>Sort
<select id="calSort">
<option value="newest">Planned order</option>
<option value="az">Title A→Z</option>
<option value="za">Title Z→A</option>
<option value="short">Shortest first</option>
<option value="long">Longest first</option>
</select>
</label>
<label>Density <input type="range" id="calDens" min="1" max="4" value="${cols}"></label>
<span style="font-size:12px;color:var(--faint)">${titles.length} videos · your first quarter</span>
</div>
<div class="calgrid" id="calGrid"></div>`;
document.documentElement.style.setProperty('--cols',cols);
const draw=()=>{
const s=$('#calSort').value;const arr=titles.map((t,i)=>({t,i}));
if(s==='az')arr.sort((a,b)=>a.t.localeCompare(b.t));
else if(s==='za')arr.sort((a,b)=>b.t.localeCompare(a.t));
else if(s==='short')arr.sort((a,b)=>a.t.length-b.t.length);
else if(s==='long')arr.sort((a,b)=>b.t.length-a.t.length);
$('#calGrid').innerHTML=arr.map(o=>`<div class="vcard"><span class="num">${o.i+1}</span><span class="vt">${esc(o.t)}</span></div>`).join('');
};
$('#calSort').value=sort;draw();
$('#calSort').onchange=()=>{prefs.sort=$('#calSort').value;localStorage.setItem(LS_CAL,JSON.stringify(prefs));draw();};
$('#calDens').oninput=()=>{const c=$('#calDens').value;document.documentElement.style.setProperty('--cols',c);prefs.cols=+c;localStorage.setItem(LS_CAL,JSON.stringify(prefs));};
}
function updateCostChip(){$('#costChip').innerHTML='session <b>'+fmtCost(sessionCost)+'</b>';}
function renderSaved(){
const all=loadProjects().sort((a,b)=>(b.updatedAt||b.createdAt).localeCompare(a.updatedAt||a.createdAt));
const wrap=$('#savedWrap'),grid=$('#savedGrid');
if(!all.length){wrap.style.display='none';return;}
wrap.style.display='block';grid.innerHTML='';
all.forEach(p=>{
const done=Object.keys(p.outputs||{}).length;const pct=Math.round(done/7*100);
const c=document.createElement('div');c.className='pcard';
c.innerHTML=`<span class="del" data-id="${p.id}" title="Delete">✕</span>
<div class="pn">${esc(p.name||'Untitled channel')}</div>
<div class="pmeta">${done}/7 steps${p.niche?' · '+esc(p.niche.slice(0,28)):''}</div>
<div class="prog"><i style="width:${pct}%"></i></div>
<div class="when" title="${esc(p.createdAt)}">🕓 ${esc(fmtWhen(p.createdAt))}</div>`;
c.onclick=(e)=>{if(!e.target.classList.contains('del'))openProject(p);};
c.querySelector('.del').onclick=(e)=>{e.stopPropagation();if(confirm('Delete "'+(p.name||'this channel')+'"?')){saveProjects(loadProjects().filter(x=>x.id!==p.id));renderSaved();}};
grid.appendChild(c);
});
}
function openProject(p){STATE=p;cur=0;show('builder');renderBar();renderStepper();renderStep();}
function startNew(){STATE=newProject();persist();cur=0;show('builder');renderBar();renderStepper();renderStep();}
function show(which){
$('#home').classList.toggle('hidden',which!=='home');
$('#builder').classList.toggle('hidden',which==='home');
if(which==='home')renderSaved();
window.scrollTo(0,0);
}
(async function(){
try{CFG=await (await fetch('/api/config')).json();}catch(e){}
const mc=$('#modeChip');
if(CFG.liveMode){mc.className='chip mode-live';mc.innerHTML='● LIVE · <b>'+CFG.model+'</b>';}
else{mc.className='chip mode-demo';mc.innerHTML='● DEMO · <b>no key</b>';}
try{const pj=await (await fetch('/api/prompts')).json();PROMPTS=pj.steps||[];}catch(e){PROMPTS=[];}
updateCostChip();
$('#startBtn').onclick=startNew;
$('#demoBtn').onclick=startNew;
$('#autoHeroBtn').onclick=()=>{ startNew(); autoBuildAll(); };
$('#homeBtn').onclick=()=>show('home');
show('home');
})();
</script>
</body>
</html>