419 lines
33 KiB
HTML
419 lines
33 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Adapt a book — PayToLaugh</title>
|
|
<meta name="description" content="Re-set a public-domain book, chapter by chapter, with a dated record of every decision you make.">
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|
<style>
|
|
.cols{display:grid;grid-template-columns:270px 1fr;gap:22px;align-items:start}
|
|
@media (max-width:900px){.cols{grid-template-columns:1fr}}
|
|
.toc{position:sticky;top:14px;max-height:calc(100vh - 28px);overflow:auto;border:1px solid var(--line);border-radius:12px;padding:10px}
|
|
.toc a{display:block;padding:7px 9px;border-radius:8px;color:var(--ink);text-decoration:none;font-size:13.5px;line-height:1.35}
|
|
.toc a:hover{background:#141a28}
|
|
.toc a.on{background:#141a28;border-left:3px solid var(--accent)}
|
|
.toc .sub{color:var(--muted);font-size:11.5px;display:block}
|
|
.pagebar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:6px 0 14px;font:13px/1.5 var(--mono,monospace);color:var(--muted)}
|
|
.pagebar b{color:var(--ink)}
|
|
.meter{height:5px;background:var(--line);border-radius:3px;flex:1;min-width:120px;overflow:hidden}
|
|
.meter i{display:block;height:100%;background:var(--accent)}
|
|
.para{padding:12px 14px;border:1px solid transparent;border-radius:10px;line-height:1.7;font-size:16px;position:relative}
|
|
.para:hover{border-color:var(--line)}
|
|
.para.sel{border-color:var(--accent);background:#10141d}
|
|
.para.changed{border-left:3px solid #3fae74}
|
|
.para .pn{position:absolute;left:-30px;top:14px;font:11px var(--mono,monospace);color:var(--muted)}
|
|
.orig{color:#5fd38d}
|
|
.new{color:var(--ink)}
|
|
.legend{font-size:12px;color:var(--muted);margin:0 0 8px}
|
|
.legend .orig{font-weight:600}
|
|
mark{border-radius:4px;padding:0 2px;cursor:pointer;color:inherit}
|
|
mark.open{background:#5a4a12}
|
|
mark.kept{background:#2a2f3a}
|
|
mark.rewritten{background:#173d2a}
|
|
.was{color:var(--muted);font-size:13.5px;margin-top:6px;border-top:1px dashed var(--line);padding-top:6px}
|
|
.work{border:1px solid var(--line);border-radius:12px;padding:16px;margin:8px 0 16px;background:var(--panel)}
|
|
.work h4{margin:0 0 6px;font-size:15px}
|
|
.work textarea{width:100%;box-sizing:border-box;min-height:70px;font:inherit;font-size:14.5px}
|
|
.work .row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;align-items:center}
|
|
.tiny{font-size:12px;color:var(--muted)}
|
|
.warn{background:#2a1512;border:1px solid #5b2a20;color:#ffb9a7;border-radius:8px;padding:8px 11px;font-size:13px;margin-top:8px}
|
|
.btn.small{padding:7px 13px;font-size:13px}
|
|
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
|
|
table.log{border-collapse:collapse;font-size:12.5px;width:100%}
|
|
table.log td,table.log th{border-top:1px solid var(--line);padding:6px 8px;vertical-align:top;text-align:left}
|
|
table.log .h{color:var(--muted);font:11px var(--mono,monospace)}
|
|
.dec li{margin:4px 0}
|
|
.tabs2{display:flex;gap:8px;margin:0 0 14px}
|
|
.tabs2 button{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:999px;padding:6px 14px;cursor:pointer;font-size:13px}
|
|
.tabs2 button.on{border-color:var(--accent);color:#fff;background:#141a28}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap" style="max-width:1180px">
|
|
<header class="page">
|
|
<h1 id="h1">Adapt a book</h1>
|
|
<p id="lede">Take a book that is in the public domain, re-set it, and keep a dated record of every decision that is yours. The reading and the finding can be done for you; the deciding and the wording are recorded as your own.</p>
|
|
</header>
|
|
<div id="msg" class="msg"></div>
|
|
|
|
<section id="pick" class="card" style="display:none">
|
|
<h2 style="margin-top:0">Start from a public-domain book</h2>
|
|
<p class="tiny">Project Gutenberg number (the number in the book's address there — The Wonderful Wizard of Oz is 55).</p>
|
|
<div class="row" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
<input id="gid" placeholder="55" style="width:90px">
|
|
<input id="written" placeholder="written in (e.g. 1900)" style="width:170px">
|
|
<input id="resetto" placeholder="re-set in (e.g. 2026)" style="width:170px">
|
|
<button class="btn" id="import" type="button">Bring it in</button>
|
|
</div>
|
|
<div id="mine" style="margin-top:18px"></div>
|
|
</section>
|
|
|
|
<section id="book" style="display:none">
|
|
<div class="tabs2">
|
|
<button type="button" data-tab="read" class="on">The manuscript</button>
|
|
<button type="button" data-tab="decisions">My creative decisions</button>
|
|
<button type="button" data-tab="scenes">Scenes</button>
|
|
<button type="button" data-tab="report">Report of changes</button>
|
|
<button type="button" data-tab="log">The record</button>
|
|
</div>
|
|
|
|
<div id="tab-scenes" style="display:none" class="card">
|
|
<h2 style="margin-top:0">Scenes in this chapter</h2>
|
|
<p class="tiny">How the chapter divides into scenes for filming: which paragraphs each covers, and so which pages. Recorded as yours.</p>
|
|
<div id="scenelist"></div>
|
|
<textarea id="scenesjson" class="tiny" style="width:100%;box-sizing:border-box;min-height:120px;font-family:var(--mono,monospace)" placeholder='[{"title":"The prairie","from":0,"to":3,"place":"EXT. FARM - DAY"}]'></textarea>
|
|
<div class="row" style="display:flex;gap:8px;margin-top:8px"><button class="btn small" type="button" id="scenesave">Save the scenes</button></div>
|
|
</div>
|
|
|
|
<div id="tab-report" style="display:none" class="card">
|
|
<h2 style="margin-top:0">Report of changes from the source</h2>
|
|
<p class="tiny">Key changes in this chapter, who made them, and how. For internal records.</p>
|
|
<div class="row" style="display:flex;gap:8px;margin:8px 0 14px"><button class="btn small" type="button" id="reportsave">Save this report as a file</button></div>
|
|
<div id="report"></div>
|
|
</div>
|
|
|
|
<div id="tab-decisions" style="display:none" class="card">
|
|
<h2 style="margin-top:0">My creative decisions</h2>
|
|
<p class="tiny">The standing choices that shape the whole adaptation, in your own words. Each is dated and kept in the record. Anything drafted for you is drafted <em>from</em> these.</p>
|
|
<ol class="dec" id="declist"></ol>
|
|
<textarea id="dectext" placeholder="e.g. The story is re-set in 2026. / The Scarecrow's wish is for an AI brain." style="width:100%;box-sizing:border-box;min-height:70px"></textarea>
|
|
<div class="row" style="display:flex;gap:8px;margin-top:8px">
|
|
<button class="btn small" type="button" id="decadd">Record this decision</button>
|
|
<button class="btn small ghost" type="button" id="ideaadd">Record it as an idea, not yet decided</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-log" style="display:none" class="card">
|
|
<h2 style="margin-top:0">The record</h2>
|
|
<p class="tiny" id="chain"></p>
|
|
<div class="row" style="display:flex;gap:8px;margin:8px 0 14px">
|
|
<button class="btn small ghost" type="button" id="logchap">This chapter only</button>
|
|
<button class="btn small ghost" type="button" id="logall">Whole book</button>
|
|
<button class="btn small" type="button" id="logsave">Save the record as a file</button>
|
|
</div>
|
|
<div style="overflow:auto"><table class="log" id="logtable"></table></div>
|
|
</div>
|
|
|
|
<div id="tab-read" class="cols">
|
|
<div class="toc" id="toc"></div>
|
|
<main>
|
|
<h2 id="chtitle" style="margin:0 0 2px"></h2>
|
|
<div class="pagebar" id="pagebar"></div>
|
|
<div id="chtools" class="row" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px"></div>
|
|
<div id="paras" style="padding-left:30px"></div>
|
|
<div class="pagebar" id="pagebar2"></div>
|
|
</main>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="/assets/app.js"></script>
|
|
<script>
|
|
(function () {
|
|
navBar("book");
|
|
if (!Session.get()) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname + location.search); return; }
|
|
var Q = new URLSearchParams(location.search);
|
|
var PLATES = [], SCENES = [], EDITION = "";
|
|
var AID = Q.get("book") || "", AD = null, TOC = [], DEC = [], CH = null, PARAS = [], REFS = [], PAGE = 1, SEL = null, LOGCHAP = true, POLL = null;
|
|
function $(id) { return document.getElementById(id); }
|
|
function msg(t, kind) { var m = $("msg"); m.className = "msg " + (kind || "err"); m.textContent = t; if (!t) m.className = "msg"; }
|
|
function when(s) { try { return new Date(s).toLocaleString(); } catch (e) { return s; } }
|
|
|
|
async function start() {
|
|
if (!AID) {
|
|
$("pick").style.display = "";
|
|
var r = await api("adapt_list", {});
|
|
$("mine").innerHTML = (r.adaptations || []).length
|
|
? '<h3 style="margin:0 0 6px">Your adaptations</h3>' + r.adaptations.map(function (a) {
|
|
return '<p><a href="?book=' + esc(a.id) + '"><b>' + esc(a.title) + '</b></a> <span class="tiny">' + esc(a.author || "") + ' · ' + esc(a.source || "") + ' · ' + (a.total_pages || "?") + ' pages · ' + esc(a.state || "") + '</span></p>'; }).join("")
|
|
: "";
|
|
return;
|
|
}
|
|
$("book").style.display = "";
|
|
await loadBook();
|
|
var n = parseInt(Q.get("ch") || "1", 10);
|
|
await openChapter(isNaN(n) ? 1 : n, parseInt(Q.get("p") || "1", 10) || 1);
|
|
}
|
|
|
|
async function loadBook() {
|
|
var r = await api("adapt_get", { adaptation_id: AID });
|
|
if (!r.ok) { msg(r.error || "That did not load."); return; }
|
|
AD = r.adaptation; TOC = r.chapters || []; DEC = r.decisions || [];
|
|
$("h1").textContent = AD.title + (AD.reset_to ? " — re-set in " + AD.reset_to : "");
|
|
$("lede").innerHTML = esc(AD.author || "") + ' · ' + (AD.source_url ? '<a href="' + esc(AD.source_url) + '" target="_blank">' + esc(AD.source) + '</a>' : esc(AD.source || ""))
|
|
+ ' · ' + (AD.total_pages || 0) + ' pages of ' + (AD.words_per_page || 250) + ' words · source fingerprint <span class="tiny">' + esc(String(AD.source_sha256 || "").slice(0, 16)) + '…</span>';
|
|
if (AD.state === "importing") { msg("Bringing the book in…", "ok"); setTimeout(loadBook, 3000); return; }
|
|
paintToc(); paintDecisions();
|
|
}
|
|
|
|
function paintToc() {
|
|
$("toc").innerHTML = TOC.map(function (c) {
|
|
var st = c.refs_state === "ready" ? (c.open ? c.open + ' to decide' : 'all decided') + ' · ' + c.changed + ' changed' : (c.refs_state === "working" ? 'being read…' : 'not read yet');
|
|
return '<a href="#" data-ch="' + c.n + '" class="' + (CH && CH.n === c.n ? 'on' : '') + '">' + (c.n ? c.n + '. ' : '') + esc(c.title)
|
|
+ '<span class="sub">pages ' + c.first_page + '–' + (c.first_page + c.pages - 1) + ' · ' + st + '</span></a>'; }).join("");
|
|
}
|
|
|
|
function paintDecisions() {
|
|
$("declist").innerHTML = DEC.length ? DEC.map(function (d) {
|
|
return '<li>' + (d.status === "idea" ? '<span class="tiny">[idea]</span> ' : '') + esc(d.text) + ' <span class="tiny">— ' + esc(d.by || "") + ', ' + esc(when(d.at)) + '</span></li>'; }).join("")
|
|
: '<li class="tiny">Nothing recorded yet.</li>';
|
|
}
|
|
|
|
async function openChapter(n, page) {
|
|
var r = await api("adapt_chapter", { adaptation_id: AID, chapter: n });
|
|
if (!r.ok) { msg(r.error || "That chapter did not load."); return; }
|
|
CH = r.chapter; PARAS = r.paragraphs || []; REFS = r.refs || []; DEC = r.decisions || DEC; PLATES = r.plates || []; SCENES = r.scenes || []; EDITION = r.edition || "";
|
|
PAGE = Math.max(1, Math.min(page || PAGE || 1, CH.pages)); SEL = null;
|
|
history.replaceState(null, "", "?book=" + encodeURIComponent(AID) + "&ch=" + n + "&p=" + PAGE);
|
|
paintToc(); paintChapter();
|
|
clearTimeout(POLL);
|
|
if (CH.refs_state === "working" || PARAS.some(function (p) { return p.drafting; }))
|
|
POLL = setTimeout(function () { openChapter(CH.n, PAGE); }, 4000);
|
|
}
|
|
|
|
// Which words of the paragraph are Baum's. Longest common subsequence on
|
|
// words (punctuation ignored for the match, kept in the output).
|
|
function tok(t) { return String(t || "").split(/(\s+)/).filter(function (x) { return x.length; }); }
|
|
function key(w) { return w.toLowerCase().replace(/[^a-z0-9']/g, ""); }
|
|
function originalMask(p) {
|
|
if (p.text === p.original) return null;
|
|
var a = tok(p.original).filter(function (x) { return !/^\s+$/.test(x); }), b = tok(p.text);
|
|
var bw = b.map(function (x, i) { return /^\s+$/.test(x) ? null : i; }).filter(function (x) { return x !== null; });
|
|
var A = a.map(key), B = bw.map(function (i) { return key(b[i]); });
|
|
var n = A.length, m = B.length, L = [];
|
|
for (var i = 0; i <= n; i++) { L.push(new Uint16Array(m + 1)); }
|
|
for (i = n - 1; i >= 0; i--) for (var j = m - 1; j >= 0; j--) L[i][j] = A[i] === B[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
|
|
var mask = b.map(function () { return false; });
|
|
i = 0; j = 0;
|
|
while (i < n && j < m) {
|
|
if (A[i] === B[j]) { mask[bw[j]] = true; i++; j++; }
|
|
else if (L[i + 1][j] >= L[i][j + 1]) i++; else j++;
|
|
}
|
|
// a lone "the" that happens to match is not Baum's sentence: only runs of
|
|
// three or more matching words count as his
|
|
var run = [];
|
|
for (var q = 0; q <= bw.length; q++) {
|
|
if (q < bw.length && mask[bw[q]]) { run.push(bw[q]); continue; }
|
|
if (run.length && run.length < 3) run.forEach(function (idx) { mask[idx] = false; });
|
|
run = [];
|
|
}
|
|
for (var k = 0; k < b.length; k++) if (/^\s+$/.test(b[k])) mask[k] = (k > 0 && mask[k - 1]) && (k + 1 < b.length && mask[k + 1]);
|
|
return { parts: b, mask: mask };
|
|
}
|
|
function greened(p) {
|
|
var om = originalMask(p);
|
|
if (!om) return '<span class="orig">' + esc(p.text) + '</span>';
|
|
var out = "", cur = null, buf = "";
|
|
om.parts.forEach(function (w, i) {
|
|
var cls = om.mask[i] ? "orig" : "new";
|
|
if (cls !== cur) { if (cur !== null) out += '<span class="' + cur + '">' + esc(buf) + '</span>'; cur = cls; buf = ""; }
|
|
buf += w;
|
|
});
|
|
if (cur !== null) out += '<span class="' + cur + '">' + esc(buf) + '</span>';
|
|
return out;
|
|
}
|
|
function marked(p) {
|
|
var text = p.text, html = greened(p), refs = REFS.filter(function (r) { return r.para === p.i; });
|
|
refs.forEach(function (r) {
|
|
html = markIn(html, r.quote, esc(r.status), esc(r.id), esc(r.why || ""));
|
|
});
|
|
return html;
|
|
}
|
|
// Wrapping a quote in <mark> inside the green spans: the quote is found in
|
|
// the plain text; the coloured html is searched for the same run of words.
|
|
function markIn(html, q, cls, id, title) {
|
|
var plain = q.split(/\s+/).map(function (w) { return esc(w).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }).join('(?:\\s|<\\/?span[^>]*>)+');
|
|
var rx = new RegExp(plain);
|
|
var m = rx.exec(html);
|
|
if (!m) return html;
|
|
return html.slice(0, m.index) + '<mark class="' + cls + '" data-ref="' + id + '" title="' + title + '">' + m[0] + '</mark>' + html.slice(m.index + m[0].length);
|
|
}
|
|
|
|
function paintChapter() {
|
|
$("chtitle").textContent = (CH.n ? "Chapter " + CH.n + " — " : "") + CH.title;
|
|
var bookPage = CH.first_page + PAGE - 1, total = AD.total_pages || 1;
|
|
var bar = '<button class="btn small ghost" type="button" data-pg="prev"' + (PAGE <= 1 && CH.n <= TOC[0].n ? ' disabled' : '') + '>◀ Back</button>'
|
|
+ '<span>page <b>' + PAGE + '/' + CH.pages + '</b> of this chapter · <b>' + bookPage + '/' + total + '</b> of the book · ' + (CH.pages - PAGE) + ' left in the chapter</span>'
|
|
+ '<span class="meter"><i style="width:' + Math.round(100 * bookPage / total) + '%"></i></span>'
|
|
+ '<button class="btn small ghost" type="button" data-pg="next">Next ▶</button>';
|
|
$("pagebar2").innerHTML = bar;
|
|
var open = REFS.filter(function (r) { return r.status === "open"; }).length;
|
|
$("chtools").innerHTML = (CH.refs_state === "working"
|
|
? '<span class="tiny">Reading the chapter for references to ' + esc(AD.written || "its own time") + '…</span>'
|
|
: '<button class="btn small' + (REFS.length ? ' ghost' : '') + '" type="button" id="findrefs">' + (REFS.length ? 'Read the chapter again' : 'Find what belongs to ' + esc(AD.written || "its time") + ' in this chapter') + '</button>')
|
|
+ (REFS.length ? '<span class="tiny">' + REFS.length + ' found · <b style="color:#f5c04b">' + open + ' for you to decide</b> · yellow = undecided, green = rewritten by you, grey = kept as written. Click one.</span>' : '')
|
|
+ (/^failed/.test(CH.refs_state || "") ? '<span class="warn">' + esc(CH.refs_state) + '</span>' : '');
|
|
var here = PARAS.filter(function (p) { return p.page === PAGE; });
|
|
var pp = here.map(function (p) { return p.print_page; }).filter(Boolean);
|
|
var printSpan = pp.length ? ' · first edition p. ' + Math.min.apply(null, pp) + (Math.max.apply(null, pp) > Math.min.apply(null, pp) ? '–' + Math.max.apply(null, pp) : '') : '';
|
|
$("pagebar").innerHTML = bar.replace('left in the chapter</span>', 'left in the chapter' + printSpan + '</span>');
|
|
var platesHere = PLATES.filter(function (x) { return pp.length && x.pg >= Math.min.apply(null, pp) && x.pg <= Math.max.apply(null, pp); });
|
|
var sceneOf = function (i) { var sc = SCENES.find(function (x) { return i >= x.from && i <= x.to; }); return sc ? '<span class="tiny" style="display:block;color:#f5c04b">Scene ' + sc.n + (sc.title ? ' · ' + esc(sc.title) : '') + '</span>' : ''; };
|
|
$("paras").innerHTML = '<p class="legend"><span class="orig">Green is Baum\'s own text, word for word.</span> White is what has been written for this adaptation.</p>' + (platesHere.length ? '<div style="display:flex;gap:10px;flex-wrap:wrap;margin:0 0 12px">' + platesHere.map(function (x) { return '<a href="' + esc(x.url) + '" target="_blank" title="' + esc(x.alt) + ' (Denslow, 1900, p.' + x.pg + ')"><img src="' + esc(x.url) + '" style="height:150px;border-radius:6px;background:#fff" alt="' + esc(x.alt) + '"></a>'; }).join("") + '</div>' : '') + here.map(function (p) {
|
|
var changed = p.text !== p.original;
|
|
return (SCENES.some(function (x) { return x.from === p.i; }) ? sceneOf(p.i) : '') + '<div class="para' + (changed ? ' changed' : '') + (SEL === p.i ? ' sel' : '') + '" data-para="' + p.i + '" title="' + (p.print_page ? 'first edition p. ' + p.print_page : '') + '"><span class="pn">' + (p.i + 1) + (p.print_page ? '<br>p' + p.print_page : '') + '</span>' + marked(p)
|
|
+ (changed ? '<div class="was"><b>As Baum wrote it:</b> ' + esc(p.original) + '<br><span class="tiny">changed ' + esc(when(p.changed_at)) + ' by ' + esc(p.changed_by || "") + ' · ' + esc(p.how || "") + ' · revision ' + p.rev + '</span></div>' : '')
|
|
+ '</div>' + (SEL === p.i ? workHtml(p) : ''); }).join("") || '<p class="tiny">Nothing on this page.</p>';
|
|
}
|
|
|
|
function workHtml(p) {
|
|
var refs = REFS.filter(function (r) { return r.para === p.i; });
|
|
var d = p.draft, list = refs.map(function (r) {
|
|
return '<div style="margin:6px 0"><mark class="' + esc(r.status) + '">' + esc(r.quote) + '</mark> <span class="tiny">' + esc(r.category || "") + ' · found by ' + esc(String(r.found_by || "").replace(/^ai:.*/, "the reader (AI)").replace(/^human:/, "")) + '</span><br><span class="tiny">' + esc(r.why || "") + '</span>'
|
|
+ (r.question ? '<br><i>' + esc(r.question) + '</i>' : '')
|
|
+ (r.status === "open" ? ' <button class="btn small ghost" type="button" data-keep="' + esc(r.id) + '">Keep it as Baum wrote it</button>' : ' <span class="tiny">— ' + esc(r.status) + (r.decided_at ? ', ' + esc(when(r.decided_at)) : '') + '</span>')
|
|
+ '</div>'; }).join("");
|
|
return '<div class="work" data-work="' + p.i + '"><h4>Paragraph ' + (p.i + 1) + ' — your decision</h4>' + (list || '<p class="tiny">Nothing was flagged here. You can still change it.</p>')
|
|
+ '<p class="tiny" style="margin:12px 0 4px">1. Say what it should become, in your own words. This is recorded as your direction.</p>'
|
|
+ '<textarea id="direction" placeholder="e.g. The farmhouse is a prefab on a wind-farm lease; the cyclone cellar is a storm shelter with a steel hatch.">' + esc((d && d.direction) || "") + '</textarea>'
|
|
+ '<div class="row"><button class="btn small ghost" type="button" data-draft="' + p.i + '"' + (p.drafting ? ' disabled' : '') + '>' + (p.drafting ? 'Drafting from your direction…' : 'Draft the wording from my direction') + '</button><span class="tiny">optional — or write it yourself below</span></div>'
|
|
+ (p.draft_error ? '<div class="warn">' + esc(p.draft_error) + '</div>' : '')
|
|
+ '<p class="tiny" style="margin:12px 0 4px">2. The paragraph as it will stand. Edit freely; what you save is recorded as yours, and the record says whether a draft was used and whether you changed it.</p>'
|
|
+ '<textarea id="newtext" style="min-height:150px">' + esc(d ? d.text : p.text) + '</textarea>'
|
|
+ (d && (d.warnings || []).length ? '<div class="warn"><b>From the 1939 film, not the book:</b><br>' + d.warnings.map(esc).join("<br>") + '</div>' : '')
|
|
+ '<div class="row"><button class="btn small" type="button" data-save="' + p.i + '">Save my wording</button>'
|
|
+ (p.text !== p.original ? '<button class="btn small ghost" type="button" data-revert="' + p.i + '">Put back Baum\'s paragraph</button>' : '')
|
|
+ '<button class="btn small ghost" type="button" data-close="1">Close</button></div></div>';
|
|
}
|
|
|
|
document.addEventListener("click", async function (ev) {
|
|
var t = ev.target, el, r;
|
|
if ((el = t.closest("[data-tab]"))) {
|
|
document.querySelectorAll(".tabs2 button").forEach(function (b) { b.classList.toggle("on", b === el); });
|
|
["read", "decisions", "log", "scenes", "report"].forEach(function (k) { $("tab-" + k).style.display = el.getAttribute("data-tab") === k ? "" : "none"; });
|
|
if (el.getAttribute("data-tab") === "log") loadLog();
|
|
if (el.getAttribute("data-tab") === "scenes") paintScenes();
|
|
if (el.getAttribute("data-tab") === "report") loadReport();
|
|
return;
|
|
}
|
|
if ((el = t.closest("[data-ch]"))) { ev.preventDefault(); openChapter(parseInt(el.getAttribute("data-ch"), 10), 1); window.scrollTo(0, 0); return; }
|
|
if ((el = t.closest("[data-pg]"))) {
|
|
var dir = el.getAttribute("data-pg") === "next" ? 1 : -1, at = TOC.findIndex(function (c) { return c.n === CH.n; });
|
|
if (PAGE + dir >= 1 && PAGE + dir <= CH.pages) { PAGE += dir; SEL = null; history.replaceState(null, "", "?book=" + AID + "&ch=" + CH.n + "&p=" + PAGE); paintChapter(); }
|
|
else if (dir > 0 && at + 1 < TOC.length) await openChapter(TOC[at + 1].n, 1);
|
|
else if (dir < 0 && at > 0) await openChapter(TOC[at - 1].n, TOC[at - 1].pages);
|
|
window.scrollTo(0, 0); return;
|
|
}
|
|
if (t.id === "findrefs") { t.disabled = true; r = await api("adapt_find_refs", { adaptation_id: AID, chapter: CH.n }); if (!r.ok) msg(r.error); await openChapter(CH.n, PAGE); return; }
|
|
if ((el = t.closest("[data-close]"))) { SEL = null; paintChapter(); return; }
|
|
if ((el = t.closest("[data-keep]"))) {
|
|
var ref = REFS.find(function (x) { return x.id === el.getAttribute("data-keep"); });
|
|
var why = prompt("Why does it stay as Baum wrote it? (recorded as your reason - may be left empty)") ;
|
|
if (why === null) return;
|
|
r = await api("adapt_keep", { adaptation_id: AID, chapter: CH.n, para: ref.para, ref_id: ref.id, reason: why });
|
|
if (!r.ok) msg(r.error); var keepSel = SEL; await openChapter(CH.n, PAGE); SEL = keepSel; paintChapter(); return;
|
|
}
|
|
if ((el = t.closest("[data-draft]"))) {
|
|
var i = parseInt(el.getAttribute("data-draft"), 10), dir2 = $("direction").value.trim();
|
|
if (dir2.length < 4) { msg("Say what it should become first - the draft is made from your direction."); return; }
|
|
var rr = REFS.find(function (x) { return x.para === i && x.status === "open"; });
|
|
r = await api("adapt_draft", { adaptation_id: AID, chapter: CH.n, para: i, ref_id: rr ? rr.id : "", direction: dir2 });
|
|
if (!r.ok) { msg(r.error); return; } msg("");
|
|
await openChapter(CH.n, PAGE); SEL = i; paintChapter(); return;
|
|
}
|
|
if ((el = t.closest("[data-save]"))) {
|
|
var j = parseInt(el.getAttribute("data-save"), 10), r2 = REFS.find(function (x) { return x.para === j && x.status === "open"; });
|
|
r = await api("adapt_rewrite", { adaptation_id: AID, chapter: CH.n, para: j, ref_id: r2 ? r2.id : "", text: $("newtext").value, direction: $("direction").value.trim() });
|
|
if (!r.ok) { msg(r.error); return; }
|
|
msg("Saved as revision " + r.rev + " (" + r.how + ")." + ((r.warnings || []).length ? " Check: " + r.warnings.join(" / ") : ""), (r.warnings || []).length ? "err" : "ok");
|
|
await loadBook(); await openChapter(CH.n, PAGE); return;
|
|
}
|
|
if ((el = t.closest("[data-revert]"))) {
|
|
var k = parseInt(el.getAttribute("data-revert"), 10);
|
|
r = await api("adapt_revert", { adaptation_id: AID, chapter: CH.n, para: k });
|
|
if (!r.ok) msg(r.error); await loadBook(); await openChapter(CH.n, PAGE); return;
|
|
}
|
|
if ((el = t.closest("mark[data-ref]"))) { var rf = REFS.find(function (x) { return x.id === el.getAttribute("data-ref"); }); if (rf) { SEL = rf.para; paintChapter(); } return; }
|
|
if ((el = t.closest(".para"))) { if (t.closest(".work")) return; SEL = parseInt(el.getAttribute("data-para"), 10); paintChapter(); return; }
|
|
if (t.id === "decadd" || t.id === "ideaadd") {
|
|
var txt = $("dectext").value.trim(); if (txt.length < 4) { msg("Write the decision in your own words."); return; }
|
|
r = await api("adapt_decision", { adaptation_id: AID, text: txt, status: t.id === "ideaadd" ? "idea" : "decided" });
|
|
if (!r.ok) { msg(r.error); return; } DEC = r.decisions; $("dectext").value = ""; paintDecisions(); msg("Recorded.", "ok"); return;
|
|
}
|
|
if (t.id === "logchap") { LOGCHAP = true; loadLog(); return; }
|
|
if (t.id === "logall") { LOGCHAP = false; loadLog(); return; }
|
|
if (t.id === "logsave") {
|
|
r = await api("adapt_log", { adaptation_id: AID });
|
|
var blob = new Blob([JSON.stringify({ adaptation: AD, decisions: DEC, chain_ok: r.chain_ok, events: r.events }, null, 1)], { type: "application/json" });
|
|
var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (AD.title || "adaptation").replace(/[^a-z0-9]+/gi, "-").toLowerCase() + "-record.json"; a.click(); return;
|
|
}
|
|
if (t.id === "import") {
|
|
var gid = $("gid").value.trim(); if (!/^\d+$/.test(gid)) { msg("Enter the Project Gutenberg number."); return; }
|
|
t.disabled = true; r = await api("adapt_import", { gutenberg_id: gid, written: $("written").value.trim(), reset_to: $("resetto").value.trim() });
|
|
if (!r.ok) { t.disabled = false; msg(r.error); return; }
|
|
location.href = "?book=" + encodeURIComponent(r.id);
|
|
}
|
|
});
|
|
|
|
function paintScenes() {
|
|
$("scenelist").innerHTML = SCENES.length ? '<ol>' + SCENES.map(function (x) { return '<li><b>' + esc(x.title || ("Scene " + x.n)) + '</b> <span class="tiny">' + esc(x.place || "") + ' · paragraphs ' + (x.from + 1) + '–' + (x.to + 1) + ' · pages ' + (x.pages || []).join(", ") + (x.print_pages && x.print_pages.length ? ' · first edition p. ' + x.print_pages[0] + (x.print_pages.length > 1 ? '–' + x.print_pages[x.print_pages.length - 1] : '') : '') + '</span>' + (x.note ? '<br><span class="tiny">' + esc(x.note) + '</span>' : '') + '</li>'; }).join("") + '</ol>' : '<p class="tiny">No scenes yet for this chapter.</p>';
|
|
$("scenesjson").value = SCENES.length ? JSON.stringify(SCENES.map(function (x) { return { title: x.title, from: x.from, to: x.to, place: x.place, note: x.note }; }), null, 1) : "";
|
|
}
|
|
var REPORT = null;
|
|
async function loadReport() {
|
|
var r = await api("adapt_report", { adaptation_id: AID, chapter: CH.n });
|
|
if (!r.ok) { msg(r.error); return; }
|
|
REPORT = r;
|
|
var people = Object.keys(r.by_person || {}).map(function (k) { var v = r.by_person[k]; return '<li><b>' + esc(k.replace(/^human:/, "").replace(/^sim:/, "")) + '</b>' + (/^sim:/.test(k) ? ' <span class="tiny">(entries made by the assistant at this person’s instruction, as a simulation)</span>' : '') + ' — ' + v.decisions + ' creative decisions, ' + v.rewrites + ' rewrites, ' + v.kept + ' kept as written, ' + v.flags + ' flagged, ' + v.reverts + ' put back</li>'; }).join("");
|
|
$("report").innerHTML = '<p><b>' + esc(r.adaptation.title) + '</b> by ' + esc(r.adaptation.author) + ' · ' + esc(r.adaptation.source) + (r.adaptation.edition ? '<br><span class="tiny">' + esc(r.adaptation.edition) + '</span>' : '') + '<br><span class="tiny">source fingerprint ' + esc(r.adaptation.source_sha256) + ' · record chain ' + (r.chain_ok ? 'intact' : 'BROKEN at ' + r.chain_broken_at) + ' · made ' + esc(when(r.made_at)) + '</span></p>'
|
|
+ '<h3>Chapter ' + r.chapter.n + ' — ' + esc(r.chapter.title) + '</h3><p class="tiny">' + r.chapter.changed + ' of ' + r.chapter.paragraphs + ' paragraphs changed · ' + r.chapter.words_original + ' words in the source, ' + r.chapter.words_final + ' now</p>'
|
|
+ '<h3>By person</h3><ul>' + (people || '<li class="tiny">nobody yet</li>') + '</ul>'
|
|
+ '<h3>Standing creative decisions</h3><ol>' + (r.decisions || []).map(function (d) { return '<li>' + esc(d.text) + ' <span class="tiny">— ' + esc(d.by) + (d.simulated ? ' (simulated)' : '') + ', ' + esc(when(d.at)) + (d.status === "idea" ? ', idea' : '') + '</span></li>'; }).join("") + '</ol>'
|
|
+ '<h3>Changed paragraphs</h3>' + (r.changed || []).map(function (c) { return '<div class="work"><b>Paragraph ' + (c.para + 1) + '</b> <span class="tiny">page ' + c.page + (c.print_page ? ' · first edition p. ' + c.print_page : '') + ' · ' + esc(c.by || "") + ' · ' + esc(c.how || "") + ' · ' + c.revisions + ' revision' + (c.revisions === 1 ? '' : 's') + '</span>' + (c.directions.length ? '<p class="tiny"><b>Direction:</b> ' + c.directions.map(esc).join(' / ') + '</p>' : '') + '<p class="tiny"><b>Source:</b> ' + esc(c.original) + '</p><p><b>Now:</b> ' + esc(c.final) + '</p></div>'; }).join("")
|
|
+ (r.kept.length ? '<h3>Kept as written, by decision</h3><ul>' + r.kept.map(function (k) { return '<li>“' + esc(k.quote) + '” <span class="tiny">paragraph ' + (k.para + 1) + (k.reason ? ' — ' + esc(k.reason) : '') + ' — ' + esc(k.by || "") + '</span></li>'; }).join("") + '</ul>' : '')
|
|
+ (r.scenes.length ? '<h3>Scenes</h3><ol>' + r.scenes.map(function (x) { return '<li>' + esc(x.title || "") + ' <span class="tiny">' + esc(x.place || "") + ' · paragraphs ' + (x.from + 1) + '–' + (x.to + 1) + '</span></li>'; }).join("") + '</ol>' : '');
|
|
}
|
|
document.addEventListener("click", async function (ev) {
|
|
var t = ev.target, r;
|
|
if (t.id === "scenesave") {
|
|
var list; try { list = JSON.parse($("scenesjson").value || "[]"); } catch (e) { msg("The scenes are not valid JSON."); return; }
|
|
r = await api("adapt_scenes", { adaptation_id: AID, chapter: CH.n, scenes: list });
|
|
if (!r.ok) { msg(r.error); return; } SCENES = r.scenes; paintScenes(); msg("Scenes saved.", "ok"); return;
|
|
}
|
|
if (t.id === "reportsave" && REPORT) {
|
|
var blob = new Blob([JSON.stringify(REPORT, null, 1)], { type: "application/json" });
|
|
var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = (AD.title || "adaptation").replace(/[^a-z0-9]+/gi, "-").toLowerCase() + "-ch" + CH.n + "-report.json"; a.click();
|
|
}
|
|
});
|
|
var NAMES = { import: "brought in", creative_decision: "creative decision", ai_flag: "found by the reader (AI)", human_flag: "found by you", human_keep: "kept as written", human_direction: "your direction", ai_draft: "draft made from your direction (AI)", human_rewrite: "your wording saved", human_revert: "put back" };
|
|
async function loadLog() {
|
|
var r = await api("adapt_log", LOGCHAP && CH ? { adaptation_id: AID, chapter: CH.n } : { adaptation_id: AID });
|
|
if (!r.ok) { msg(r.error); return; }
|
|
$("chain").innerHTML = (r.chain_ok ? "Every entry carries the fingerprint of the one before it, and the chain is intact" : "<b style='color:#e0685f'>The chain is broken at entry " + r.chain_broken_at + "</b>") + " · " + r.events.length + " entries" + (LOGCHAP && CH ? " for chapter " + CH.n : "") + ". Times are as recorded by the server (UTC), shown in your local time.";
|
|
$("logtable").innerHTML = '<tr><th>#</th><th>when</th><th>who</th><th>what</th><th>where</th><th>detail</th></tr>' + r.events.map(function (e) {
|
|
var who = String(e.actor || "").replace(/^human:/, "").replace(/^ai:/, "AI · ").replace(/^sim:(.*)$/, "$1 (simulated by the assistant)");
|
|
var detail = (e.direction ? '<b>Direction:</b> ' + esc(e.direction) + '<br>' : '')
|
|
+ (e.kind === "human_rewrite" || e.kind === "ai_draft" || e.kind === "human_revert" ? '<span class="tiny">before:</span> ' + esc(e.before_text) + '<br><span class="tiny">after:</span> ' + esc(e.after_text) + '<br>' : '')
|
|
+ (e.kind === "creative_decision" ? esc(e.after_text) + '<br>' : '')
|
|
+ (e.kind === "ai_flag" || e.kind === "human_flag" || e.kind === "human_keep" ? '“' + esc(e.original_text) + '”<br>' : '')
|
|
+ (e.note ? '<span class="tiny">' + esc(e.note) + '</span>' : '');
|
|
return '<tr><td class="h">' + e.seq + '</td><td class="h">' + esc(when(e.at)) + '</td><td>' + esc(who) + '</td><td>' + esc(NAMES[e.kind] || e.kind) + (e.iteration ? ' <span class="tiny">rev ' + e.iteration + '</span>' : '') + '</td><td class="h">' + (e.chapter != null && e.chapter !== "" ? 'ch ' + e.chapter + (e.page ? ' p' + e.page : '') + (e.para != null && e.para !== "" ? ' ¶' + (Number(e.para) + 1) : '') : '') + '</td><td>' + detail + '<div class="h">' + esc(String(e.hash || "").slice(0, 16)) + '…</div></td></tr>'; }).join("");
|
|
}
|
|
|
|
start();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|