Initial authoring system prototype
This commit is contained in:
72
prototype/static/app.js
vendored
Normal file
72
prototype/static/app.js
vendored
Normal 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) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[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'; });
|
||||
64
prototype/static/index.html
Normal file
64
prototype/static/index.html
Normal 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) …"></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>
|
||||
14
prototype/static/styles.css
Normal file
14
prototype/static/styles.css
Normal 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; } }
|
||||
Reference in New Issue
Block a user