Initial authoring system prototype

This commit is contained in:
Joey Grasty
2026-08-23 14:27:56 -05:00
commit 23e4ef595e
101 changed files with 5473 additions and 0 deletions

72
prototype/static/app.js vendored Normal file
View File

@@ -0,0 +1,72 @@
let project;
const byId = (id) => document.getElementById(id);
const selected = (container) => [...container.querySelectorAll('input:checked')].map((input) => input.value);
function checks(container, records) {
container.innerHTML = records.map((record) => `
<label title="${escapeHtml(record.text)}"><input type="checkbox" value="${record.uuid}"> ${escapeHtml(record.title)}</label>
`).join('');
}
function escapeHtml(value) {
return value.replace(/[&<>'"]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[character]));
}
function updateSceneState() {
const scene = project.scenes.find((candidate) => candidate.uuid === byId('scene').value);
byId('scene-state').textContent = scene ? `${scene.chapter} · ${scene.state} · ${scene.brief ? 'BRIEF paired' : 'BRIEF missing'}` : '';
byId('brief').textContent = scene && scene.brief ? scene.brief : 'No paired BRIEF exists for this scene.';
}
function render(data) {
project = data;
byId('status').textContent = data.locked ? 'Scrivener lock detected — read-only' : 'Project not locked — app remains read-only';
byId('status').className = `status ${data.locked ? 'warning' : 'ok'}`;
byId('project-check').innerHTML = [
['Scenes', data.scenes.length],
['Briefs without scene', data.unmatched_briefs.length],
['Scenes without brief', data.unmatched_scenes.length],
['Characters', data.characters.length],
['Places', data.places.length],
].map(([label, value]) => `<dt>${label}</dt><dd>${value}</dd>`).join('');
byId('scene').innerHTML = data.scenes.map((scene) => `<option value="${scene.uuid}">${escapeHtml(scene.title)}${escapeHtml(scene.chapter)}</option>`).join('');
checks(byId('characters'), data.characters);
checks(byId('places'), data.places);
updateSceneState();
}
async function load() {
const response = await fetch('/api/project');
if (!response.ok) throw new Error('Could not load the Scrivener project.');
render(await response.json());
}
async function build() {
byId('error').textContent = '';
const payload = {
scene_uuid: byId('scene').value,
character_uuids: selected(byId('characters')),
place_uuids: selected(byId('places')),
lexicon: byId('lexicon').value,
story: byId('story').value,
recall: byId('recall').value,
previous: byId('previous').value,
notes: byId('notes').value,
directive: byId('directive').value,
new_thread: byId('new-thread').checked,
};
const response = await fetch('/api/package-preview', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) {
byId('error').textContent = data.error || 'Could not build package.';
return;
}
byId('package').textContent = data.package;
}
byId('scene').addEventListener('change', updateSceneState);
byId('build').addEventListener('click', build);
load().catch((error) => { byId('status').textContent = error.message; byId('status').className = 'status warning'; });

View File

@@ -0,0 +1,64 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Authoring System — Package Preview</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<header>
<div>
<p class="eyebrow">READ-ONLY PROTOTYPE</p>
<h1>Package Preview</h1>
</div>
<div id="status" class="status">Loading project…</div>
</header>
<main>
<aside>
<h2>Project check</h2>
<dl id="project-check"></dl>
<h2>Scene</h2>
<label for="scene">Paired Manuscript scene</label>
<select id="scene"></select>
<p id="scene-state" class="muted"></p>
<h2>Characters</h2>
<div id="characters" class="checks"></div>
<h2>Places for NOTES</h2>
<div id="places" class="checks"></div>
</aside>
<section class="workspace">
<section class="brief-panel">
<h2>Author BRIEF <span class="read-only">read-only</span></h2>
<pre id="brief">Select a scene to view its paired BRIEF.</pre>
</section>
<div class="grid">
<label>LEXICON (manual subset)
<textarea id="lexicon" rows="4" placeholder="Term — usage note"></textarea>
</label>
<label>STORY (manual preview)
<textarea id="story" rows="4" placeholder="Leave empty for a new thread in this prototype."></textarea>
</label>
<label>RECALL (manual preview)
<textarea id="recall" rows="4" placeholder="(Book 1 — summary)&#10;…"></textarea>
</label>
<label>PREVIOUS (manual preview)
<textarea id="previous" rows="4" placeholder="Verbatim paragraph-boundary tail."></textarea>
</label>
<label>Additional NOTES
<textarea id="notes" rows="4" placeholder="Scene-specific facts or exact text."></textarea>
</label>
<label>DIRECTIVE
<textarea id="directive" rows="4" placeholder="Expand to a maximum of 1500 words. Bring the scene to a close…"></textarea>
</label>
</div>
<label class="checkbox"><input id="new-thread" type="checkbox" checked> Treat empty STORY/PREVIOUS as a new narrative thread</label>
<button id="build">Build package preview</button>
<p id="error" class="error"></p>
<h2>Exact package</h2>
<pre id="package">Select a scene, choose context, then build a preview.</pre>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
:root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #17212b; background: #f4f1ea; }
* { box-sizing: border-box; }
body { margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; padding: 1.4rem 2.2rem; background: #1d3f44; color: #fff; }
h1 { margin: 0; font-size: 1.8rem; } h2 { margin: 1.5rem 0 .6rem; font-size: 1rem; } .eyebrow { margin: 0 0 .25rem; font-size: .7rem; letter-spacing: .12em; color: #c7ded6; }
main { display: grid; grid-template-columns: 300px minmax(0, 1fr); min-height: calc(100vh - 84px); }
aside { padding: 1.2rem 1.5rem; border-right: 1px solid #d8d2c5; background: #fcfaf5; } .workspace { padding: 1.5rem 2rem; }
label { display: block; font-size: .85rem; font-weight: 650; } select, textarea { width: 100%; margin-top: .35rem; border: 1px solid #bab2a5; border-radius: 4px; background: #fffefb; padding: .55rem; font: .9rem ui-monospace, SFMono-Regular, Menlo, monospace; }
select { font-family: inherit; } .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } .checks label { padding: .2rem 0; font-weight: 400; } .checks input { margin-right: .35rem; }
button { margin: 1rem 0; border: 0; border-radius: 4px; padding: .65rem 1rem; background: #b54c2d; color: white; font-weight: 700; cursor: pointer; } button:hover { background: #923b22; }
pre { min-height: 28rem; max-height: 65vh; overflow: auto; margin: 0; padding: 1rem; border: 1px solid #d8d2c5; background: #202525; color: #e5f3ed; white-space: pre-wrap; font: .82rem/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
.brief-panel { margin-bottom: 1.25rem; padding: 0 1rem 1rem; border: 1px solid #d8d2c5; background: #fcfaf5; } .brief-panel h2 { margin-top: 1rem; } .brief-panel pre { min-height: 0; max-height: 16rem; background: #fffefb; color: #17212b; font-family: inherit; font-size: .95rem; } .read-only { font-size: .68rem; font-weight: 600; color: #665f54; text-transform: uppercase; letter-spacing: .08em; }
dl { display: grid; grid-template-columns: 1fr auto; margin: 0; font-size: .9rem; } dt, dd { margin: 0; padding: .25rem 0; border-bottom: 1px solid #ece6da; } dd { font-weight: 700; } .status { border-radius: 999px; padding: .45rem .75rem; font-size: .78rem; } .ok { background: #d9eee4; color: #164d38; } .warning { background: #ffe2d6; color: #7f2c19; } .muted { color: #665f54; font-size: .8rem; } .error { color: #aa2717; font-weight: 700; } .checkbox { margin-top: 1rem; font-weight: 400; }
@media (max-width: 800px) { main { grid-template-columns: 1fr; } aside { border-right: 0; border-bottom: 1px solid #d8d2c5; } .grid { grid-template-columns: 1fr; } }