v1.4.0: Organic writing, speed, and log improvements
Organic book quality: - write_chapter: strip key_events spoilers from character context so the writer doesn't know planned future events when writing early chapters - write_chapter: added next_chapter_hint — seeds anticipation for the next scene in the final paragraphs of each chapter for natural story flow - write_chapter: added DIALOGUE VOICE instruction referencing CHARACTER TRACKING speech styles so every character sounds distinctly different - Lowered SCORE_AUTO_ACCEPT 9→8 to stop over-refining already-professional drafts Speed improvements: - check_pacing: reduced from every chapter to every other chapter (~50% fewer calls) - refine_persona: reduced from every 3 to every 5 chapters (~40% fewer calls) - Resume summary rebuild: uses first + last-4 chapters instead of all chapters to avoid massive prompts when resuming mid-book - Summary context sent to writer capped at 8000 chars (most-recent events) - update_tracking text cap lowered 500000→20000 (covers any realistic chapter) Logging and progress bars: - Progress bar updates at chapter START, not just after completion - Chapter banner logged before each write so the log shows which chapter is active - Word count logged after first draft (e.g. "Draft: 2,341 words (target: ~2200)") - Word count added to chapter completion TIMING line - Pacing check now logs "Pacing OK" with reason when no intervention needed - utils: added log_banner() helper for phase separator lines UI: - run_details.html: log lines are now phase-coloured (WRITER=cyan, ARCHITECT=green, TIMING=gray, SYSTEM=yellow, TRACKER=purple, RESUME=orange, etc.) - Status bar shows current active phase (e.g. "Status: Running — WRITER") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -337,20 +337,69 @@
|
||||
const statusText = document.getElementById('status-text');
|
||||
const statusBar = document.getElementById('status-bar');
|
||||
const costEl = document.getElementById('run-cost');
|
||||
|
||||
|
||||
let lastLog = '';
|
||||
|
||||
// Phase → colour mapping (matches utils.log phase labels)
|
||||
const PHASE_COLORS = {
|
||||
'WRITER': '#4fc3f7',
|
||||
'ARCHITECT': '#81c784',
|
||||
'TIMING': '#78909c',
|
||||
'SYSTEM': '#fff176',
|
||||
'TRACKER': '#ce93d8',
|
||||
'RESUME': '#ffb74d',
|
||||
'SERIES': '#64b5f6',
|
||||
'ENRICHER': '#4dd0e1',
|
||||
'HARVESTER': '#ff8a65',
|
||||
'EDITOR': '#f48fb1',
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function colorizeLog(logText) {
|
||||
if (!logText) return '';
|
||||
return logText.split('\n').map(line => {
|
||||
const m = line.match(/^(\[[\d:]+\])\s+(\w+)\s+\|(.*)$/);
|
||||
if (!m) return '<span style="color:#666">' + escapeHtml(line) + '</span>';
|
||||
const [, ts, phase, msg] = m;
|
||||
const color = PHASE_COLORS[phase] || '#aaaaaa';
|
||||
return '<span style="color:#555">' + escapeHtml(ts) + '</span> '
|
||||
+ '<span style="color:' + color + ';font-weight:bold">' + phase.padEnd(14) + '</span>'
|
||||
+ '<span style="color:#ccc">|' + escapeHtml(msg) + '</span>';
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
function getCurrentPhase(logText) {
|
||||
if (!logText) return '';
|
||||
const lines = logText.split('\n').filter(l => l.trim());
|
||||
for (let k = lines.length - 1; k >= 0; k--) {
|
||||
const m = lines[k].match(/\]\s+(\w+)\s+\|/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function updateLog() {
|
||||
fetch(`/run/${runId}/status`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Update Status Text
|
||||
statusText.innerText = "Status: " + data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
// Update Status Text + current phase
|
||||
const statusLabel = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||
if (data.status === 'running') {
|
||||
const phase = getCurrentPhase(data.log);
|
||||
statusText.innerText = 'Status: Running' + (phase ? ' — ' + phase : '');
|
||||
} else {
|
||||
statusText.innerText = 'Status: ' + statusLabel;
|
||||
}
|
||||
costEl.innerText = '$' + parseFloat(data.cost).toFixed(4);
|
||||
|
||||
|
||||
// Update Status Bar
|
||||
if (data.status === 'running' || data.status === 'queued') {
|
||||
statusBar.className = "progress-bar progress-bar-striped progress-bar-animated";
|
||||
statusBar.style.width = (data.percent || 5) + "%";
|
||||
|
||||
|
||||
let label = (data.percent || 0) + "%";
|
||||
if (data.status === 'running' && data.percent > 2 && data.start_time) {
|
||||
const elapsed = (Date.now() / 1000) - data.start_time;
|
||||
@@ -371,15 +420,16 @@
|
||||
statusBar.innerText = "";
|
||||
}
|
||||
|
||||
// Update Log (only if changed to avoid scroll jitter)
|
||||
if (consoleEl.innerText !== data.log) {
|
||||
// Update Log with phase colorization (only if changed to avoid scroll jitter)
|
||||
if (lastLog !== data.log) {
|
||||
lastLog = data.log;
|
||||
const isScrolledToBottom = consoleEl.scrollHeight - consoleEl.clientHeight <= consoleEl.scrollTop + 50;
|
||||
consoleEl.innerText = data.log;
|
||||
consoleEl.innerHTML = colorizeLog(data.log);
|
||||
if (isScrolledToBottom) {
|
||||
consoleEl.scrollTop = consoleEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Poll if running
|
||||
if (data.status === 'running' || data.status === 'queued') {
|
||||
setTimeout(updateLog, 2000);
|
||||
|
||||
Reference in New Issue
Block a user