← 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&nbsp;<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
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,'&quot;').replace(/</g,'&lt;');}
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>