- Each chapter card now has a footer with Prev/Next chapter anchor links - First chapter shows only Next; last chapter shows 'End of Book' - Back to Top link on every chapter footer - Added get_chapter_neighbours() helper in story/bible_tracker.py for programmatic chapter sequence navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
9.5 KiB
HTML
228 lines
9.5 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4 sticky-top bg-white py-3 border-bottom" style="z-index: 100;">
|
|
<div>
|
|
<h3 class="mb-0"><i class="fas fa-book-reader me-2"></i>{{ book_folder }}</h3>
|
|
<small class="text-muted">Run #{{ run.id }}</small>
|
|
</div>
|
|
<div>
|
|
<form action="{{ url_for('run.sync_book_metadata', run_id=run.id, book_folder=book_folder) }}" method="POST" class="d-inline me-2" onsubmit="return confirm('This will re-scan your manuscript to update the character list and author persona. Continue?');">
|
|
<button type="submit" class="btn btn-outline-info" data-bs-toggle="tooltip" title="Scans your manual edits to update the character database and author writing style. Use this after making significant edits.">
|
|
<i class="fas fa-sync me-2"></i>Sync Metadata
|
|
</button>
|
|
</form>
|
|
<a href="{{ url_for('run.view_run', id=run.id) }}" class="btn btn-outline-secondary">Back to Run</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
{% for ch in manuscript %}
|
|
<div class="card shadow-sm mb-5" id="ch-{{ ch.num }}">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Chapter {{ ch.num }}: {{ ch.title }}</h5>
|
|
<div>
|
|
<button class="btn btn-sm btn-outline-warning me-1" data-bs-toggle="modal" data-bs-target="#rewriteModal{{ ch.num|string|replace(' ', '') }}" data-bs-toggle="tooltip" title="Ask AI to rewrite this chapter based on new instructions.">
|
|
<i class="fas fa-magic"></i> Rewrite
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary" onclick="toggleEdit('{{ ch.num }}')">
|
|
<i class="fas fa-edit"></i> Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Mode -->
|
|
<div class="card-body chapter-content" id="view-{{ ch.num }}">
|
|
<div class="prose" style="font-family: 'Georgia', serif; font-size: 1.1rem; line-height: 1.6; color: #333;">
|
|
{{ ch.html_content|safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Mode -->
|
|
<div class="card-body d-none" id="edit-{{ ch.num }}">
|
|
<textarea class="form-control font-monospace" id="text-{{ ch.num }}" rows="20">{{ ch.content }}</textarea>
|
|
<div class="d-flex justify-content-end mt-2">
|
|
<button class="btn btn-secondary me-2" onclick="toggleEdit('{{ ch.num }}')">Cancel</button>
|
|
<button class="btn btn-success" onclick="saveChapter('{{ ch.num }}')">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chapter Navigation Footer -->
|
|
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center py-2">
|
|
{% if not loop.first %}
|
|
{% set prev_ch = manuscript[loop.index0 - 1] %}
|
|
<a href="#ch-{{ prev_ch.num }}" class="btn btn-sm btn-outline-secondary">
|
|
<i class="fas fa-arrow-up me-1"></i>Ch {{ prev_ch.num }}
|
|
</a>
|
|
{% else %}
|
|
<span></span>
|
|
{% endif %}
|
|
<a href="#" class="btn btn-sm btn-link text-muted small py-0">Back to Top</a>
|
|
{% if not loop.last %}
|
|
{% set next_ch = manuscript[loop.index0 + 1] %}
|
|
<a href="#ch-{{ next_ch.num }}" class="btn btn-sm btn-outline-secondary">
|
|
Ch {{ next_ch.num }}<i class="fas fa-arrow-down ms-1"></i>
|
|
</a>
|
|
{% else %}
|
|
<span class="text-muted small fst-italic">End of Book</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Rewrite Modal -->
|
|
<div class="modal fade" id="rewriteModal{{ ch.num|string|replace(' ', '') }}" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Rewrite Chapter {{ ch.num }} with AI</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Instructions</label>
|
|
<textarea name="instruction" class="form-control" rows="4" placeholder="e.g. 'Change the setting to a train station', 'Make the protagonist refuse the offer', 'Fix the pacing in the middle section'." required></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-warning" onclick="startRewrite('{{ ch.num }}')">Rewrite Chapter</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Table of Contents Sidebar -->
|
|
<div class="col-lg-3 d-none d-lg-block">
|
|
<div class="sticky-top" style="top: 100px;">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">Table of Contents</div>
|
|
<div class="list-group list-group-flush" style="max-height: 70vh; overflow-y: auto;">
|
|
{% for ch in manuscript %}
|
|
<a href="#ch-{{ ch.num }}" class="list-group-item list-group-item-action small">
|
|
{{ ch.num }}. {{ ch.title }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Modal -->
|
|
<div class="modal fade" id="progressModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-body text-center p-4">
|
|
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;"></div>
|
|
<h4>Processing AI Request...</h4>
|
|
<p class="text-muted mb-0">This may take a few minutes, especially if subsequent chapters need updates. Please wait.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let rewritePollInterval = null;
|
|
|
|
function toggleEdit(num) {
|
|
const viewDiv = document.getElementById(`view-${num}`);
|
|
const editDiv = document.getElementById(`edit-${num}`);
|
|
|
|
if (editDiv.classList.contains('d-none')) {
|
|
editDiv.classList.remove('d-none');
|
|
viewDiv.classList.add('d-none');
|
|
} else {
|
|
editDiv.classList.add('d-none');
|
|
viewDiv.classList.remove('d-none');
|
|
}
|
|
}
|
|
|
|
function startRewrite(num) {
|
|
const modal = document.getElementById(`rewriteModal${String(num).replace(' ', '')}`);
|
|
const instruction = modal.querySelector('textarea[name="instruction"]').value;
|
|
|
|
if (!instruction) {
|
|
alert("Please provide an instruction for the AI.");
|
|
return;
|
|
}
|
|
|
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
modalInstance.hide();
|
|
|
|
const progressModal = new bootstrap.Modal(document.getElementById('progressModal'));
|
|
progressModal.show();
|
|
|
|
const data = {
|
|
book_folder: "{{ book_folder }}",
|
|
chapter_num: num,
|
|
instruction: instruction
|
|
};
|
|
|
|
fetch(`/project/{{ run.id }}/rewrite_chapter`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error("Failed to start rewrite task.");
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
if (data.task_id) {
|
|
rewritePollInterval = setInterval(() => pollRewriteStatus(data.task_id), 3000);
|
|
} else {
|
|
throw new Error("Did not receive task ID.");
|
|
}
|
|
})
|
|
.catch(err => {
|
|
progressModal.hide();
|
|
alert("Error: " + err.message);
|
|
});
|
|
}
|
|
|
|
function pollRewriteStatus(taskId) {
|
|
fetch(`/task_status/${taskId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'completed') {
|
|
clearInterval(rewritePollInterval);
|
|
setTimeout(() => { window.location.reload(); }, 500);
|
|
}
|
|
})
|
|
.catch(err => { clearInterval(rewritePollInterval); alert("Error checking status. Please reload manually."); });
|
|
}
|
|
|
|
function saveChapter(num) {
|
|
const content = document.getElementById(`text-${num}`).value;
|
|
const btn = event.target;
|
|
const originalText = btn.innerText;
|
|
|
|
btn.disabled = true;
|
|
btn.innerText = "Saving...";
|
|
|
|
const formData = new FormData();
|
|
formData.append('book_folder', "{{ book_folder }}");
|
|
formData.append('chapter_num', num);
|
|
formData.append('content', content);
|
|
|
|
fetch(`/project/{{ run.id }}/save_chapter`, {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(res => {
|
|
if (res.ok) {
|
|
alert("Chapter saved! Reloading to render changes...");
|
|
window.location.reload();
|
|
} else {
|
|
alert("Error saving chapter.");
|
|
btn.disabled = false;
|
|
btn.innerText = originalText;
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |