Add script review page with feedback loop and approval

This commit is contained in:
gator 2026-09-05 19:29:34 +00:00
parent 5d0ba261fb
commit 34150bb264
2 changed files with 176 additions and 19 deletions

143
script.html Normal file
View file

@ -0,0 +1,143 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Review the script — PayToLaugh</title>
<meta name="description" content="Read the script, ask for changes, approve it when it sounds right.">
<link rel="stylesheet" href="/assets/app.css">
<style>
.seg{border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:14px;background:#0f1117}
.seg .top{display:flex;justify-content:space-between;gap:14px;align-items:baseline;margin-bottom:10px;flex-wrap:wrap}
.seg .n{font:600 11px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
.seg .cam{font:11px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.seg .line{font-size:18px;line-height:1.5;margin:0}
.seg .foot{font:11.5px/1 var(--mono);color:var(--muted);margin-top:10px}
.warn{background:#2a2112;border:1px solid #5b4520;color:#f0d9a7;border-radius:10px;padding:13px 16px;margin-bottom:16px;font-size:14px}
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1 id="title">The script</h1>
<p id="sub">Read it through. If it does not sound like your pet, say so and we will rewrite it.</p>
</header>
<section>
<div class="msg" id="msg"></div>
<div id="out"><p style="color:var(--muted)">Loading…</p></div>
</section>
<footer>PayToLaugh &middot; your pet has opinions.</footer>
</div>
<script src="/assets/app.js"></script>
<script>
navBar("videos");
var email = requireSession();
var pid = new URLSearchParams(location.search).get("id");
var poll = null;
if (!pid) {
document.getElementById("out").innerHTML =
'<div class="empty">No script chosen. <a href="/videos.html">Back to my videos</a>.</div>';
}
function segHtml(s) {
return '<div class="seg">'
+ '<div class="top"><span class="n">Shot ' + (s.index || "") + '</span>'
+ '<span class="cam">' + esc(s.camera || "") + '</span></div>'
+ '<p class="line">' + esc(s.line || "") + '</p>'
+ '<div class="foot">' + (s.word_count || 0) + ' words · about ' + (s.est_seconds || 0) + 's'
+ (s.note ? ' · ' + esc(s.note) : '') + '</div>'
+ '</div>';
}
function render(r) {
var p = r.project, s = r.script, out = document.getElementById("out");
var used = parseInt(p.revisions || 0, 10);
var left = (r.max_revisions || 3) - used;
if (p.status === "scripting") {
out.innerHTML = '<div class="card"><p style="margin:0;color:var(--muted)">'
+ 'Writing the script now. This usually takes under a minute — the page will update itself.</p></div>';
if (!poll) poll = setInterval(load, 5000);
return;
}
if (poll) { clearInterval(poll); poll = null; }
if (p.status === "script_failed") {
out.innerHTML = '<div class="card"><p style="margin:0 0 16px">The writer could not finish this one.</p>'
+ '<a class="btn" style="display:inline-block;text-decoration:none" href="/create.html">Start again</a></div>';
return;
}
if (!s || !s.segments) {
out.innerHTML = '<div class="empty">No script on this project yet.</div>';
return;
}
document.getElementById("title").textContent = s.title || "The script";
document.getElementById("sub").textContent = s.premise || "";
var approved = p.status === "approved";
var warn = (s._warnings && s._warnings.length)
? '<div class="warn"><strong>Worth a look:</strong> ' + s._warnings.map(esc).join(" · ") + '</div>'
: "";
var head = '<div class="quote" style="margin-bottom:22px">'
+ '<div><div class="big">$' + Number(p.price_usd || 0).toFixed(2) + '</div>'
+ '<div class="sub">' + ((p.segments || 4) * 8) + ' seconds · ' + (p.segments || 4) + ' shots</div></div>'
+ '<div class="sub">' + (approved ? "Approved" : left + " rewrite" + (left === 1 ? "" : "s") + " left") + '</div>'
+ '</div>';
var actions = approved
? '<div class="card"><p style="margin:0"><strong>Approved.</strong> Filming is switched on once payment is live. '
+ 'Nothing has been charged.</p></div>'
: '<div class="card">'
+ '<h2 class="sec">Not quite right?</h2>'
+ '<div class="field">'
+ '<label for="fb">What would you change</label>'
+ '<span class="hint">Be specific. “Too polite, he would be smugger about it” beats “make it funnier”.</span>'
+ '<textarea id="fb" ' + (left <= 0 ? "disabled" : "") + ' placeholder="The third shot is too wordy, and he would never admit the robot is good at anything."></textarea>'
+ '</div>'
+ '<button class="btn ghost" id="revise" ' + (left <= 0 ? "disabled" : "") + '>'
+ (left > 0 ? "Rewrite it" : "No rewrites left") + '</button>'
+ '<button class="btn" id="approve" style="margin-left:10px">Approve this script</button>'
+ '</div>';
out.innerHTML = head + warn + s.segments.map(segHtml).join("") + actions;
var rv = document.getElementById("revise");
if (rv) rv.onclick = async function () {
hideMsg("msg");
var fb = document.getElementById("fb").value.trim();
if (fb.length < 5) { showMsg("msg", "Tell us what you would like changed."); return; }
rv.disabled = true; rv.textContent = "Sending…";
var res = await api("revise_script", { email: email, project_id: pid, feedback: fb });
if (!res.ok) { rv.disabled = false; rv.textContent = "Rewrite it"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", "Rewriting now…", "ok");
load();
};
var ap = document.getElementById("approve");
if (ap) ap.onclick = async function () {
hideMsg("msg");
ap.disabled = true; ap.textContent = "Approving…";
var res = await api("approve_script", { email: email, project_id: pid });
if (!res.ok) { ap.disabled = false; ap.textContent = "Approve this script"; showMsg("msg", res.error || "That did not go through."); return; }
showMsg("msg", res.status_text || "Approved.", "ok");
load();
};
}
async function load() {
if (!pid || !email) return;
var r = await api("get_project", { email: email, project_id: pid });
if (!r.ok) { showMsg("msg", r.error || "Could not load that script."); return; }
render(r);
}
load();
</script>
</body>
</html>

View file

@ -29,12 +29,14 @@ navBar("videos");
var email = requireSession();
var LABEL = {
submitted: ["submitted", "In the queue"],
scripting: ["submitted", "Being written"],
script_ready: ["submitted", "Script ready for you"],
rendering: ["submitted", "Filming"],
ready: ["ready", "Ready"],
failed: ["failed", "Did not work"]
submitted: ["submitted", "In the queue"],
scripting: ["submitted", "Writing the script"],
script_ready: ["ready", "Script ready to review"],
script_failed:["failed", "Script did not write"],
approved: ["ready", "Approved, waiting to film"],
rendering: ["submitted", "Filming"],
ready: ["ready", "Ready"],
failed: ["failed", "Did not work"]
};
function when(s) {
@ -48,10 +50,23 @@ function card(p) {
var media = p.video_url
? '<video controls playsinline preload="metadata" src="' + esc(p.video_url) + '"></video>'
: '<div class="ph">Not filmed yet</div>';
var dl = p.video_url
? '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="'
+ esc(p.video_url) + '" download>Download</a>'
: "";
var cta = "";
if (p.status === "script_ready") {
cta = '<a class="btn" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
+ encodeURIComponent(p.id) + '">Review the script</a>';
} else if (p.status === "scripting") {
cta = '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
+ encodeURIComponent(p.id) + '">Watch it being written</a>';
} else if (p.status === "approved" || p.status === "script_failed") {
cta = '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px" href="/script.html?id='
+ encodeURIComponent(p.id) + '">See the script</a>';
}
if (p.video_url) {
cta += '<a class="btn ghost" style="display:inline-block;text-decoration:none;margin-top:10px;margin-left:8px" href="'
+ esc(p.video_url) + '" download>Download</a>';
}
return '<div class="item">'
+ '<div>' + media + '</div>'
+ '<div>'
@ -59,28 +74,27 @@ function card(p) {
+ '<h3>' + esc(p.pet_name || "Your pet") + ' reacts</h3>'
+ '<p class="meta">' + (p.source_url ? '<a href="' + esc(p.source_url) + '" rel="noopener noreferrer nofollow" target="_blank">' + esc(p.source_url) + '</a>' : "") + '</p>'
+ '<p class="meta">' + esc(p.owner_notes || "") + '</p>'
+ '<p class="meta">' + (p.segments || 4) * 8 + ' seconds · '
+ '<p class="meta">' + ((p.segments || 4) * 8) + ' seconds · '
+ (p.price_usd != null ? "$" + Number(p.price_usd).toFixed(2) : "") + ' · ' + esc(when(p.created_at)) + '</p>'
+ dl
+ cta
+ '</div></div>';
}
(async function () {
async function load() {
if (!email) return;
var r = await api("list_projects", { email: email });
var out = document.getElementById("out");
if (!r.ok) {
out.innerHTML = "";
showMsg("msg", r.error || "Could not load your videos.");
return;
}
if (!r.ok) { out.innerHTML = ""; showMsg("msg", r.error || "Could not load your videos."); return; }
if (!r.projects || !r.projects.length) {
out.innerHTML = '<div class="empty"><p style="margin:0 0 16px">Nothing here yet.</p>'
+ '<a class="btn" style="display:inline-block;text-decoration:none" href="/create.html">Make your first video</a></div>';
return;
}
out.innerHTML = '<div class="list">' + r.projects.map(card).join("") + '</div>';
})();
// Anything still being written will change shortly; refresh so the button appears.
if (r.projects.some(function (p) { return p.status === "scripting"; })) setTimeout(load, 6000);
}
load();
</script>
</body>
</html>