From a20ed2d57b0a80481290f133ce0b35954fdb90db Mon Sep 17 00:00:00 2001
From: gator
Date: Thu, 17 Sep 2026 03:08:17 +0000
Subject: [PATCH] trailer.html: pick a head writer, read the reviews, revise,
film
The four scripts had no next step. Everything after "read them" lived in the
API and nothing on the page reached it.
The choice of head writer is the author's, and it is a judgement about concept
rather than a score, so it is a button on each tab rather than a leaderboard.
Naming the lead first also changes what the reviews are for: three models
arguing about how to fix THIS script is a more useful argument than three
models arguing about whose is better.
Reviews are split the way they come back - errors are things that are wrong
and are worth fixing, suggestions are matters of judgement, and "keeps" is
what a revision must not quietly remove. The author's shot notes outrank all
of it and the revise button says so.
Film is priced and gated on having a storyboard, because every shot needs an
opening frame to film from.
---
trailer.html | 213 +++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 174 insertions(+), 39 deletions(-)
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 '