diff --git a/trailer.html b/trailer.html index 5d230cb..bcff926 100644 --- a/trailer.html +++ b/trailer.html @@ -11,8 +11,10 @@ .tab{border:1px solid var(--line);border-bottom:0;background:none;color:var(--muted); padding:10px 16px;border-radius:10px 10px 0 0;cursor:pointer;font:inherit;font-size:14px} .tab.on{color:var(--ink);border-color:var(--line);background:var(--panel)} + .tab.lead{color:var(--accent)} .tab .sc{color:var(--accent2);margin-left:7px;font-weight:600} .tab .st{color:var(--muted);margin-left:7px;font-size:12px} + .tab .crown{margin-left:6px} .head{display:flex;gap:18px;flex-wrap:wrap;align-items:baseline;margin-bottom:6px} .head h2{margin:0;font-size:22px} .tag{color:var(--muted);font-style:italic} @@ -59,6 +61,15 @@ border-radius:999px;padding:0 6px} .vobadge{color:var(--muted);font-size:11px;border:1px solid var(--line); border-radius:999px;padding:0 6px} + .revs{margin:0 0 22px} + .rev{border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px} + .rev h4{margin:0 0 6px;font-size:14px} + .rev h4 .n{color:var(--accent2);font-weight:600;margin-left:8px} + .rev .v{color:var(--muted);font-size:13px;margin:0 0 8px} + .rev ul{margin:4px 0 8px;padding-left:18px;font-size:13px} + .rev .er{color:#e0685f} + .rev .kp{color:var(--accent2)} + .rev .sh{color:var(--muted);font-variant-numeric:tabular-nums} @@ -80,44 +91,59 @@ if (!requireSession()) return; navBar("trailer"); - var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, timer = null; + var RUN = null, DRAFTS = [], CUR = null, PKG = {}, NOTES = {}, REVIEWS = []; + var timer = null; var runId = new URLSearchParams(location.search).get("run") || ""; var audio = new Audio(); if (!runId) { showMsg("msg", "No run was named in the link."); return; } - // A draft is busy while it is being written or drawn. Both are minutes long - // and both report progress on the row, so one poll covers them. - function busyState(d) { + // A draft is busy while it is being written or drawn; a run is busy while it + // is reviewing or revising. Both are minutes long and report on the row. + function busyDraft(d) { var s = String(d.draw_state || ""); return d.state === "writing" || (s && !/^(drawn|failed)/.test(s)); } + function busyRun() { + return RUN && /^(writing|reviewing|revising)$/.test(String(RUN.stage || "")); + } async function load(repaint) { var r = await api("run_status", { run_id: runId }); if (!r.ok) { showMsg("msg", r.error || "Could not load that run."); return; } - var wasBusy = DRAFTS.length && DRAFTS.some(busyState); + var wasBusy = (DRAFTS.length && DRAFTS.some(busyDraft)) || busyRun(); RUN = r.run; DRAFTS = r.drafts || []; document.getElementById("title").textContent = RUN.book_title || "Trailers"; document.getElementById("sub").textContent = sub(); - if (!CUR && DRAFTS.length) CUR = DRAFTS[0].id; + if (!CUR && DRAFTS.length) CUR = leadDraftId() || DRAFTS[0].id; paintTabs(); - var busy = DRAFTS.some(busyState); + var rv = await api("list_reviews", { run_id: runId }); + REVIEWS = (rv && rv.reviews) || []; + + var busy = DRAFTS.some(busyDraft) || busyRun(); if (busy && !timer) timer = setInterval(load, 5000); if (!busy && timer) { clearInterval(timer); timer = null; } var me = DRAFTS.filter(function (d) { return d.id === CUR; })[0]; if (me && me.state === "writing") { paintWriting(); return; } - // Repaint when asked, on first paint, or when work has just finished. if (repaint !== false && (wasBusy || !document.querySelector(".shot"))) openTab(CUR); else paintBar(); } + function leadDraftId() { + var best = null; + DRAFTS.forEach(function (d) { + if (d.writer === RUN.lead_writer + && (!best || Number(d.iteration) > Number(best.iteration))) best = d; + }); + return best && best.id; + } + function sub() { var bits = []; if (RUN.stage) bits.push(RUN.stage); - bits.push(DRAFTS.length + " writer" + (DRAFTS.length === 1 ? "" : "s")); + bits.push(DRAFTS.length + " script" + (DRAFTS.length === 1 ? "" : "s")); var spent = DRAFTS.reduce(function (a, d) { return a + Number(d.write_usd || 0) + Number(d.draw_usd || 0); }, 0); if (spent) bits.push("$" + spent.toFixed(2) + " so far"); @@ -133,16 +159,19 @@ : (s && !/^(drawn|failed)/.test(s)) ? '' + esc(s) + '' : d.score ? '' + d.score + '' : ''; + var isLead = d.writer === RUN.lead_writer; return ''; }).join(""); } function paintWriting() { document.getElementById("pane").innerHTML = - '

Each writer is reading the whole book and writing its own ' - + 'trailer. This takes a few minutes.

'; + '

Working. Reading the whole book takes a few minutes.

'; } document.getElementById("tabs").addEventListener("click", function (e) { @@ -160,7 +189,9 @@ var pane = document.getElementById("pane"); if (meta.state === "failed") { pane.innerHTML = '

' + esc(meta.label) + ' did not finish.
' - + esc(meta.note || "No reason was recorded.") + '

'; + + esc(meta.note || "No reason was recorded.") + '

' + + '
'; return; } if (meta.state === "writing") { paintWriting(); return; } @@ -172,50 +203,109 @@ pane.innerHTML = paneHtml(meta, r.draft); } - function drawBtn(meta) { + function barHtml(meta) { var shots = (PKG.shots || []).length, cast = (PKG.characters || []).length; var imgs = cast * 3 + shots * 2; var price = imgs * 0.068; var s = String(meta.draw_state || ""); - if (s && !/^(drawn|failed)/.test(s)) { - return 'Cast sheets first, then every shot. ' - + 'This takes a few minutes.'; + var drawing = s && !/^(drawn|failed)/.test(s); + var drawn = /^drawn/.test(s); + var isLead = meta.writer === RUN.lead_writer; + var out = []; + + // 1. Whose concept wins. The author's call, not a score's. + if (!isLead) { + out.push(''); + } else { + out.push('★ head writer'); } - if (/^drawn/.test(s)) { - return '' - + '' + esc(s) - + (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) + ' spent' : '') - + ''; + + // 2. Drawing. + if (drawing) { + out.push(''); + } else { + out.push(''); + if (!drawn) out.push('' + imgs + ' pictures'); + else out.push('' + esc(s) + + (meta.draw_usd ? ' · $' + Number(meta.draw_usd).toFixed(2) : '') + ''); } - return '' - + '' + imgs + ' pictures — ' + cast - + ' cast sheets and ' + shots + ' shots, opening and closing.' - + (s === "failed" && meta.note ? '' + esc(meta.note) + '' : ''); + + // 3. Review, revise, film - only meaningful on the head writer's tab. + if (isLead) { + var others = DRAFTS.filter(function (d) { + return d.writer !== RUN.lead_writer && /^ready/.test(String(d.state)); }).length; + if (RUN.stage === "reviewing") { + out.push(''); + } else if (others) { + out.push(''); + } + if (RUN.stage === "revising") { + out.push(''); + } else if (REVIEWS.length || Object.keys(NOTES).length) { + out.push(''); + } + if (drawn) { + out.push(''); + } else { + out.push('Draw the storyboard before filming — ' + + 'every shot is filmed from its opening frame.'); + } + } + return out.join(""); } function paintBar() { var el = document.querySelector(".bar"); var meta = DRAFTS.filter(function (d) { return d.id === CUR; })[0]; - if (el && meta) el.innerHTML = drawBtn(meta); + if (el && meta) el.innerHTML = barHtml(meta); + } + + function reviewsHtml(meta) { + var mine = REVIEWS.filter(function (r) { + return r.subject === meta.writer && Number(r.iteration) === Number(meta.iteration); }); + if (!mine.length) return ""; + return '
' + mine.map(function (r) { + var parts = ['

' + esc(r.label) + + '' + esc(r.score) + '/100

']; + function list(items, cls, fmt) { + if (!items || !items.length) return ""; + return ''; + } + parts.push(list(r.errors, "er", function (e) { + return '
  • ' + (e.shot ? 'shot ' + esc(e.shot) + ' ' : '') + + esc(e.problem || "") + (e.fix ? ' — ' + esc(e.fix) + '' : '') + '
  • '; })); + parts.push(list(r.suggestions, "", function (s) { + return '
  • ' + (s.shot ? 'shot ' + esc(s.shot) + ' ' : '') + + esc(s.change || "") + (s.why ? ' (' + esc(s.why) + ')' : '') + '
  • '; })); + parts.push(list(r.strengths, "kp", function (s) { + return '
  • ' + esc(typeof s === "string" ? s : JSON.stringify(s)) + '
  • '; })); + return parts.join("") + '
    '; + }).join("") + '
    '; } function paneHtml(meta, draft) { var p = PKG, shots = p.shots || [], cast = p.characters || []; var voice = {}; cast.forEach(function (c) { voice[c.id] = c; }); - var secs = shots.reduce(function (a, s) { return a + (Number(s.edit_seconds) || 0); }, 0); var head = '

    ' + esc(p.title || meta.label) + '

    ' + (p.tagline ? '' + esc(p.tagline) + '' : '') + '
    ' + (p.logline ? '

    ' + esc(p.logline) + '

    ' : '') - + '

    ' + esc(meta.label) + ' · ' + shots.length + ' shots · ' + + '

    ' + esc(meta.label) + + (Number(meta.iteration) > 1 ? ' · revision ' + meta.iteration : '') + + ' · ' + shots.length + ' shots · ' + Math.floor(secs / 60) + ':' + String(Math.round(secs % 60)).padStart(2, "0") + ' · ' + cast.length + ' characters' + (meta.write_usd ? ' · $' + Number(meta.write_usd).toFixed(2) + ' to write' : '') + + (meta.note ? ' · ' + esc(meta.note) : '') + '

    '; var probs = ""; @@ -240,7 +330,8 @@ + '
    ' + esc((c.appearance || "").slice(0, 150)) + '
    '; }).join("") + ''; - return head + probs + '
    ' + drawBtn(meta) + '
    ' + castHtml + return head + probs + '
    ' + barHtml(meta) + '
    ' + + reviewsHtml(meta) + castHtml + shots.map(function (s) { return shotHtml(s, voice); }).join(""); } @@ -274,7 +365,7 @@ + (s.continuity ? '

    continuity: ' + esc(s.continuity) + '

    ' : '') + lines + '' + ''; } @@ -282,17 +373,61 @@ // ------------------------------------------------------------ actions document.getElementById("pane").addEventListener("click", async function (e) { - var draw = e.target.closest ? e.target.closest("[data-draw]") : null; + var t = e.target.closest ? e.target : null; + if (!t) return; + + var lead = e.target.closest("[data-lead]"); + if (lead) { + lead.disabled = true; + var a = await api("set_lead", { run_id: runId, draft_id: lead.getAttribute("data-lead") }); + if (!a.ok) { lead.disabled = false; showMsg("msg", a.error || ""); return; } + hideMsg("msg"); await load(); openTab(CUR); return; + } + + var draw = e.target.closest("[data-draw]"); if (draw) { draw.disabled = true; var d = await api("draw_storyboard", { draft_id: draw.getAttribute("data-draw") }); - if (!d.ok) { draw.disabled = false; showMsg("msg", d.error || "That did not start."); return; } - hideMsg("msg"); - load(); + if (!d.ok) { draw.disabled = false; showMsg("msg", d.error || ""); return; } + hideMsg("msg"); load(); return; + } + + var rev = e.target.closest("[data-review]"); + if (rev) { + rev.disabled = true; + var v = await api("start_review", { run_id: runId }); + if (!v.ok) { rev.disabled = false; showMsg("msg", v.error || ""); return; } + hideMsg("msg"); load(); return; + } + + var rvs = e.target.closest("[data-revise]"); + if (rvs) { + rvs.disabled = true; + var w = await api("start_revision", { run_id: runId }); + if (!w.ok) { rvs.disabled = false; showMsg("msg", w.error || ""); return; } + hideMsg("msg"); load(); return; + } + + var film = e.target.closest("[data-film]"); + if (film) { + film.disabled = true; + var f = await api("film_draft", { draft_id: film.getAttribute("data-film") }); + if (!f.ok) { film.disabled = false; showMsg("msg", f.error || ""); return; } + showMsg("msg", "Filming " + f.shots + " shots, about $" + f.usd + + ". It appears in My videos as it goes.", "ok"); + setTimeout(function () { location.href = "/videos.html"; }, 2500); return; } - var btn = e.target.closest ? e.target.closest(".play") : null; + var retry = e.target.closest("[data-retry]"); + if (retry) { + retry.disabled = true; + var y = await api("retry_writer", { draft_id: retry.getAttribute("data-retry") }); + if (!y.ok) { retry.disabled = false; showMsg("msg", y.error || ""); return; } + hideMsg("msg"); load(); return; + } + + var btn = e.target.closest(".play"); if (!btn) return; var sid = btn.getAttribute("data-shot"); btn.disabled = true; btn.innerHTML = "…";