Files
bookapp/templates/project.html
Mike Wichers f740174257 feat: Add project deletion; untrack CLAUDE.md from git
- Add DELETE /project/<id>/delete route with ownership check, active-run
  guard, filesystem cleanup (shutil.rmtree), and StoryState cascade delete
- Add delete button + confirmation modal to project page header
- Add delete button + per-project confirmation modal to dashboard cards
- Add CLAUDE.md to .gitignore and remove it from git tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:32:09 -05:00

768 lines
41 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<div class="d-flex align-items-center">
<h1 class="mb-0 me-3">{{ project.name }}</h1>
{% if not locked and not is_refining %}
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editProjectModal"><i class="fas fa-edit"></i></button>
{% endif %}
<button class="btn btn-sm btn-outline-info ms-2" data-bs-toggle="modal" data-bs-target="#cloneProjectModal" title="Clone/Fork Project" data-bs-toggle="tooltip">
<i class="fas fa-code-branch"></i>
</button>
{% if not locked %}
<button class="btn btn-sm btn-outline-danger ms-2" data-bs-toggle="modal" data-bs-target="#deleteProjectModal" title="Delete Project">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
<div class="mt-2">
<span class="badge bg-secondary">{{ bible.project_metadata.genre }}</span>
<span class="badge bg-info text-dark">{{ bible.project_metadata.target_audience }}</span>
</div>
</div>
<div>
<form action="/project/{{ project.id }}/run" method="POST" class="d-inline">
<button class="btn btn-success shadow px-4 py-2" {% if active_runs %}disabled{% endif %} data-bs-toggle="tooltip" title="Start the AI writer. It will write the next book in the plan.">
<i class="fas fa-play me-2"></i>{{ 'Generating...' if active_runs else 'Generate New Book' }}
</button>
</form>
{% for ar in active_runs %}
<form action="/run/{{ ar.id }}/stop" method="POST" class="d-inline ms-2">
<button class="btn btn-danger shadow px-3 py-2" title="Stop Run #{{ ar.id }}" onclick="return confirm('Stop Run #{{ ar.id }}? If the server restarted, this will simply unlock the UI.')">
<i class="fas fa-stop me-1"></i>#{{ ar.id }}
</button>
</form>
{% endfor %}
</div>
</div>
<!-- Workflow Help -->
<div class="alert alert-light border shadow-sm mb-4">
<div class="d-flex align-items-center">
<i class="fas fa-info-circle text-primary fa-2x me-3"></i>
<div>
<strong>Workflow:</strong>
1. Review the <a href="#bible-section">World Bible</a> below. &nbsp;&nbsp;
2. Click <span class="badge bg-success">Generate New Book</span>. &nbsp;&nbsp;
3. When finished, <a href="#latest-run">Download</a> the files or click <span class="badge bg-primary">Read & Edit</span> to refine the text.
</div>
</div>
</div>
<!-- ACTIVE JOBS CARD — shows all currently running/queued jobs -->
{% if active_runs %}
<div class="card mb-4 border-0 shadow-sm border-start border-warning border-4">
<div class="card-header bg-warning bg-opacity-10 border-0 pt-3 px-4 pb-2">
<h5 class="mb-0"><i class="fas fa-spinner fa-spin text-warning me-2"></i>Active Jobs ({{ active_runs|length }})</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for ar in active_runs %}
<div class="list-group-item d-flex align-items-center px-4 py-3">
<span class="badge bg-{{ 'warning text-dark' if ar.status == 'queued' else 'primary' }} me-3">{{ ar.status|upper }}</span>
<div class="flex-grow-1">
<strong>Run #{{ ar.id }}</strong>
<span class="text-muted ms-2 small">Started: {{ ar.start_time.strftime('%Y-%m-%d %H:%M') if ar.start_time else 'Pending' }}</span>
{% if ar.progress %}
<div class="progress mt-1" style="height: 6px; max-width: 200px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ ar.progress }}%"></div>
</div>
{% endif %}
</div>
<a href="{{ url_for('run.view_run', id=ar.id) }}" class="btn btn-sm btn-outline-primary me-2">
<i class="fas fa-eye me-1"></i>View Details
</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- LATEST RUN CARD -->
<div class="card mb-4 border-0 shadow-sm" id="latest-run">
<div class="card-header bg-white border-bottom-0 pt-4 px-4">
<h4 class="card-title"><i class="fas fa-bolt text-warning me-2"></i>Active Run (ID: {{ active_run.id if active_run else '-' }})</h4>
</div>
<div class="card-body px-4 pb-4">
{% if active_run %}
<div class="d-flex align-items-center mb-3">
<span class="badge bg-{{ 'success' if active_run.status == 'completed' else 'warning' if active_run.status in ['running', 'queued'] else 'danger' if active_run.status in ['failed', 'cancelled', 'interrupted'] else 'secondary' }} fs-6 me-3 status-badge-{{ active_run.id }}">
{{ active_run.status|upper }}
</span>
<span class="text-muted small">Run #{{ active_run.id }} started at {{ active_run.start_time.strftime('%Y-%m-%d %H:%M') }}</span>
<span class="ms-auto text-muted fw-bold">Cost: $<span class="cost-{{ active_run.id }}">{{ "%.4f"|format(active_run.cost) }}</span></span>
</div>
<!-- DOWNLOADS -->
{% if artifacts or cover_image %}
<div class="row mt-3">
{% if cover_image %}
<div class="col-md-4 text-center mb-3">
<div class="card shadow-sm">
<img src="/project/{{ active_run.id }}/download?file={{ cover_image }}&t={{ active_run.end_time.timestamp() if active_run.end_time else 0 }}" class="card-img-top" alt="Book Cover">
</div>
</div>
{% endif %}
<div class="col-md-{{ '8' if cover_image else '12' }}">
<div class="alert alert-{{ 'success' if active_run.status == 'completed' else 'warning' }} h-100">
<h5 class="alert-heading"><i class="fas fa-{{ 'check-circle' if active_run.status == 'completed' else 'folder-open' }} me-2"></i>{{ 'Book Generated!' if active_run.status == 'completed' else 'Files Available' }}</h5>
<p>{{ 'Your manuscript and ebook files are ready.' if active_run.status == 'completed' else 'Files generated during this run (may be partial).' }}</p>
<hr>
<div class="d-flex flex-wrap gap-2">
{% for file in artifacts %}
<a href="/project/{{ active_run.id }}/download?file={{ file.path }}" class="btn btn-success">
<i class="fas fa-download me-1"></i> Download {{ file.type }}
</a>
{% endfor %}
<button class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#regenerateModal" data-bs-toggle="tooltip" title="Re-create the cover art or re-compile the EPUB without rewriting the text.">
<i class="fas fa-paint-brush me-1"></i> Regenerate Cover / Files
</button>
</div>
{% if blurb_content %}
<div class="card mt-3 border-0 bg-light">
<div class="card-body">
<h6 class="card-title fw-bold text-muted">Back Cover Blurb</h6>
<p class="card-text fst-italic">{{ blurb_content }}</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<!-- STATUS BAR -->
{% if active_run and active_run.status in ['running', 'queued'] %}
<div class="card bg-light border-0 mb-3" id="statusBar">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="spinner-border text-primary spinner-border-sm me-2" role="status"></div>
<strong class="text-primary" id="statusPhase">Initializing...</strong>
<button type="button" class="btn btn-sm btn-outline-secondary ms-auto py-0" onclick="fetchLog()" title="Manually refresh status">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<h5 class="card-title mb-3" id="statusMessage">Preparing environment...</h5>
<div class="progress" style="height: 20px;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted mt-2 d-block" id="statusTime"></small>
</div>
</div>
{% endif %}
<!-- CONSOLE -->
<div class="mt-3">
<button class="btn btn-sm btn-link text-decoration-none p-0 mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#consoleCollapse">
<i class="fas fa-terminal me-1"></i> {{ 'Hide' if active_run.status in ['running', 'queued'] else 'Show' }} Console Logs
</button>
<div class="collapse {{ 'show' if active_run.status in ['running', 'queued'] else '' }}" id="consoleCollapse">
<div id="consoleOutput" class="console-log bg-dark text-success p-3 rounded" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">Loading logs...</div>
</div>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<p>No books generated yet.</p>
<p>Click the <strong>Generate New Book</strong> button to start writing.</p>
</div>
{% endif %}
</div>
</div>
<!-- RUN HISTORY -->
<div class="card mb-4 border-0 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-history me-2"></i>Run History</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Date</th>
<th>Status</th>
<th>Cost</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for r in runs %}
<tr class="{{ 'table-primary' if active_run and r.id == active_run.id else '' }}">
<td>#{{ r.id }}</td>
<td>{{ r.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<span class="badge bg-{{ 'success' if r.status == 'completed' else 'warning' if r.status in ['running', 'queued'] else 'danger' if r.status in ['failed', 'cancelled', 'interrupted'] else 'secondary' }}">
{{ r.status }}
</span>
</td>
<td>${{ "%.4f"|format(r.cost) }}</td>
<td>
<a href="{{ url_for('run.view_run', id=r.id) }}" class="btn btn-sm btn-outline-primary">
{{ 'View Active' if active_run and r.id == active_run.id and active_run.status in ['running', 'queued'] else 'View' }}
</a>
{% if r.status in ['failed', 'cancelled', 'interrupted'] %}
<form action="/run/{{ r.id }}/restart" method="POST" class="d-inline ms-1">
<input type="hidden" name="mode" value="resume">
<button class="btn btn-sm btn-outline-success" title="Resume from last step">
<i class="fas fa-play"></i>
</button>
</form>
{% endif %}
{% if r.status not in ['running', 'queued'] %}
<form action="/run/{{ r.id }}/restart" method="POST" class="d-inline ms-1" onsubmit="return confirm('This will delete all files for this run and start over. Are you sure?');">
<input type="hidden" name="mode" value="restart_clean">
<button class="btn btn-sm btn-outline-danger" title="Re-run (Wipe & Restart)">
<i class="fas fa-redo"></i>
</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted">No runs found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- SERIES OVERVIEW -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">
<i class="fas fa-{{ 'layer-group' if bible.project_metadata.is_series else 'book' }} me-2"></i>
{{ 'Series Overview' if bible.project_metadata.is_series else 'Book Overview' }}
</h4>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addBookModal">
<i class="fas fa-plus me-1"></i> {{ 'Add Book' if bible.project_metadata.is_series else 'Extend to Series' }}
</button>
</div>
<div class="row flex-nowrap overflow-auto pb-3" style="gap: 1rem;">
{% for book in bible.books %}
<div class="col-md-4 col-lg-3" style="min-width: 300px;">
<div class="card h-100 shadow-sm border-top border-4 {{ 'border-success' if generated_books.get(book.book_number) else 'border-secondary' }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-light text-dark border">Book {{ book.book_number }}</span>
{% if generated_books.get(book.book_number) %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-check me-1"></i>Generated
</button>
<ul class="dropdown-menu">
{% set gb = generated_books.get(book.book_number) %}
{% if gb.epub %}<li><a class="dropdown-item" href="/project/{{ gb.run_id }}/download?file={{ gb.epub }}"><i class="fas fa-file-epub me-2"></i>Download EPUB</a></li>{% endif %}
{% if gb.docx %}<li><a class="dropdown-item" href="/project/{{ gb.run_id }}/download?file={{ gb.docx }}"><i class="fas fa-file-word me-2"></i>Download DOCX</a></li>{% endif %}
</ul>
</div>
{% else %}
<span class="badge bg-secondary">Planned</span>
{% endif %}
</div>
<h5 class="card-title text-truncate" title="{{ book.title }}">{{ book.title }}</h5>
<p class="card-text small text-muted" style="height: 60px; overflow: hidden;">
{{ book.manual_instruction or "No summary provided." }}
</p>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editBookModal{{ book.book_number }}" title="Edit Details" {% if is_refining %}disabled{% endif %}>
<i class="fas fa-edit"></i> Edit
</button>
{% if not locked %}
{% if not generated_books.get(book.book_number) %}
<form action="/project/{{ project.id }}/delete_book/{{ book.book_number }}" method="POST" onsubmit="return confirm('Remove this book from the plan?');">
<button class="btn btn-sm btn-outline-danger" {% if is_refining %}disabled{% endif %}><i class="fas fa-trash"></i></button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- Edit Book Modal (Specific to this book) -->
<div class="modal fade" id="editBookModal{{ book.book_number }}" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/book/{{ book.book_number }}/update" method="POST">
<div class="modal-header">
<h5 class="modal-title">Edit Book {{ book.book_number }}</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">Title</label>
<input type="text" name="title" class="form-control" value="{{ book.title }}" {% if locked %}disabled{% endif %}>
</div>
<div class="mb-3">
<label class="form-label">Plot Summary / Instruction</label>
<textarea name="instruction" class="form-control" rows="6" {% if locked %}disabled{% endif %}>{{ book.manual_instruction }}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
{% if not locked %}
<button type="submit" class="btn btn-primary">Save Changes</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- Add Book Card -->
{% if not locked and not is_refining %}
<div class="col-md-4 col-lg-3" style="min-width: 200px;">
<div class="card h-100 border-dashed d-flex align-items-center justify-content-center bg-light" style="border: 2px dashed #ccc; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#addBookModal">
<div class="text-center text-muted py-5">
<i class="fas fa-plus-circle fa-2x mb-2"></i>
<br>Add Next Book
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- WORLD BIBLE & LINKED SERIES -->
<div class="row mb-4" id="bible-section">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-globe me-2"></i>World Bible & Characters</h5>
<div>
{% if is_refining %}
<span class="badge bg-warning text-dark me-2">
<span class="spinner-border spinner-border-sm me-1"></span> Refining...
</span>
{% endif %}
<a href="/project/{{ project.id }}/review" class="btn btn-sm btn-outline-info me-1">
<i class="fas fa-list-alt me-1"></i> Full Review
</a>
{% if not locked and not is_refining %}
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#importCharModal" data-bs-toggle="tooltip" title="Import characters from another project to create a shared universe.">
<i class="fas fa-link me-1"></i> Link / Import Series
</button>
{% if has_draft %}
<a href="/project/{{ project.id }}/bible_comparison" class="btn btn-sm btn-warning ms-1 fw-bold">
<i class="fas fa-balance-scale me-1"></i> Review Draft
</a>
{% elif is_refining %}
<button class="btn btn-sm btn-outline-secondary ms-1" disabled>
<i class="fas fa-magic me-1"></i> Refining...
</button>
{% else %}
<button class="btn btn-sm btn-outline-primary ms-1" data-bs-toggle="modal" data-bs-target="#refineBibleModal" data-bs-toggle="tooltip" title="Use AI to bulk-edit characters or plot points based on your instructions.">
<i class="fas fa-magic me-1"></i> Refine
</button>
{% endif %}
{% endif %}
</div>
</div>
<div class="card-body">
{% if has_draft %}
<div class="alert alert-warning shadow-sm mb-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-exclamation-circle me-2"></i>
<strong>Draft Pending:</strong> You have an unreviewed Bible refinement waiting.
</div>
<a href="/project/{{ project.id }}/bible_comparison" class="btn btn-warning btn-sm fw-bold">Review Changes</a>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-4 border-end">
<h6 class="text-muted text-uppercase small fw-bold mb-3">Project Metadata</h6>
<dl class="row small mb-0">
<dt class="col-4">Author</dt><dd class="col-8">{{ bible.project_metadata.author }}</dd>
<dt class="col-4">Genre</dt><dd class="col-8">{{ bible.project_metadata.genre }}</dd>
<dt class="col-4">Tone</dt><dd class="col-8">{{ bible.project_metadata.style.tone }}</dd>
<dt class="col-4">Series</dt><dd class="col-8">{{ 'Yes' if bible.project_metadata.is_series else 'No' }}</dd>
</dl>
</div>
<div class="col-md-8">
<h6 class="text-muted text-uppercase small fw-bold mb-3">Characters ({{ bible.characters|length }})</h6>
<div class="d-flex flex-wrap gap-2">
{% for c in bible.characters %}
<span class="badge bg-light text-dark border" title="{{ c.description }}">
<i class="fas fa-user me-1 text-secondary"></i> {{ c.name }}
<span class="text-muted fw-normal">| {{ c.role }}</span>
</span>
{% else %}
<span class="text-muted small">No characters defined.</span>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/update" method="POST">
<div class="modal-header">
<h5 class="modal-title">Edit Project Details</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">Book Title</label><input type="text" name="title" class="form-control" value="{{ bible.project_metadata.title }}" required></div>
<div class="mb-3"><label class="form-label">Author Name</label><input type="text" name="author" class="form-control" value="{{ bible.project_metadata.author }}" required></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<!-- Clone Project Modal -->
<div class="modal fade" id="cloneProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/clone" method="POST" onsubmit="showLoading(this)">
<div class="modal-header">
<h5 class="modal-title">Clone & Modify Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Create a new project based on this one, with AI modifications.</p>
<div class="mb-3"><label class="form-label">New Project Name</label><input type="text" name="new_name" class="form-control" value="{{ project.name }} (Copy)" required></div>
<div class="mb-3"><label class="form-label">AI Instructions (Optional)</label><textarea name="instruction" class="form-control" rows="3" placeholder="e.g. 'Change the genre to Sci-Fi', 'Make the protagonist a villain', 'Rewrite as a comedy'."></textarea></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-info">Clone Project</button>
</div>
</form>
</div>
</div>
<!-- Add Book Modal -->
<div class="modal fade" id="addBookModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/add_book" method="POST">
<div class="modal-header">
<h5 class="modal-title">Add Book to Series</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">Book Title</label>
<input type="text" name="title" class="form-control" placeholder="e.g. The Two Towers" required>
</div>
<div class="mb-3">
<label class="form-label">Plot Summary / Instruction</label>
<textarea name="instruction" class="form-control" rows="4" placeholder="Briefly describe the plot. Mention if it follows the main cast or shifts to side characters/new settings."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Book</button>
</div>
</form>
</div>
</div>
<!-- Import Characters Modal -->
<div class="modal fade" id="importCharModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/import_characters" method="POST">
<div class="modal-header">
<h5 class="modal-title">Link Related Series</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Import characters from another project to establish a shared universe.</p>
<div class="mb-3">
<label class="form-label">Source Project</label>
<select name="source_project_id" class="form-select" required>
<option value="">-- Select Project --</option>
{% for p in other_projects %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Import Characters</button>
</div>
</form>
</div>
</div>
<!-- Regenerate/Feedback Modal -->
<div class="modal fade" id="regenerateModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ active_run.id if active_run else 0 }}/regenerate_artifacts" method="POST">
<div class="modal-header">
<h5 class="modal-title">Update Files & Cover</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">This will regenerate the EPUB/DOCX files with any metadata changes. You can also provide feedback to fix the cover.</p>
<div class="mb-3">
<label class="form-label">Cover Feedback (Optional)</label>
<textarea name="feedback" class="form-control" rows="3" placeholder="e.g. 'Change the title color to gold', 'The text is hard to read', or 'I don't like the image, make it darker'."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Regenerate</button>
</div>
</form>
</div>
</div>
<!-- Refine Bible Modal -->
<div class="modal fade" id="refineBibleModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" onsubmit="submitRefineModal(event); return false;" action="javascript:void(0);">
<div class="modal-header">
<h5 class="modal-title">Refine Bible with AI</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Ask the AI to change characters, plot points, or settings.</p>
<div class="mb-3">
<label class="form-label">Instruction</label>
<textarea name="instruction" class="form-control" rows="3" placeholder="e.g. 'Change the ending of Book 1', 'Add a character named Bob', 'Make the tone darker'." required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Bible</button>
</div>
</form>
</div>
</div>
<!-- Delete Project Modal -->
<div class="modal fade" id="deleteProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/delete" method="POST">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2"></i>Delete Project</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>This will permanently delete <strong>{{ project.name }}</strong> and all its runs, files, and generated books.</p>
<p class="text-danger fw-bold">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete Project</button>
</div>
</form>
</div>
</div>
<!-- Full Bible JSON Modal -->
<div class="modal fade" id="fullBibleModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Full Bible JSON</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body bg-light">
<pre class="small">{{ bible | tojson(indent=2) }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let activeInterval = null;
// Only auto-poll if we have a latest run
let currentRunId = {{ active_run.id if active_run else 'null' }};
const initialRunStatus = "{{ active_run.status if active_run else '' }}";
function fetchLog() {
if (!currentRunId) return;
fetch(`/run/${currentRunId}/status`)
.then(response => response.json())
.then(data => {
const consoleDiv = document.getElementById('consoleOutput');
if (consoleDiv) {
consoleDiv.innerText = data.log || "Waiting for logs...";
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
// Update Sidebar Badge & Cost
const badge = document.querySelector(`.status-badge-${currentRunId}`);
if (badge) {
badge.innerText = data.status.toUpperCase();
badge.className = `badge fs-6 me-3 status-badge-${currentRunId} bg-${data.status === 'completed' ? 'success' : (data.status === 'running' || data.status === 'queued') ? 'warning' : (data.status === 'failed' || data.status === 'interrupted' || data.status === 'cancelled') ? 'danger' : 'secondary'}`;
}
const costSpan = document.querySelector(`.cost-${currentRunId}`);
if (costSpan) costSpan.innerText = parseFloat(data.cost).toFixed(4);
// Update Progress Bar Width
const progBar = document.getElementById('progressBar');
if (progBar && data.percent !== undefined) {
progBar.style.width = data.percent + "%";
let label = data.percent + "%";
if (data.status === 'running' && data.percent > 2 && data.start_time) {
const elapsed = (Date.now() / 1000) - data.start_time;
if (elapsed > 5) {
const remaining = (elapsed / (data.percent / 100)) - elapsed;
const m = Math.floor(remaining / 60);
const s = Math.floor(remaining % 60);
if (remaining > 0 && remaining < 86400) label += ` (~${m}m ${s}s)`;
}
}
progBar.innerText = label;
}
// Update Status Bar
if (data.progress && data.progress.message) {
const phaseEl = document.getElementById('statusPhase');
const msgEl = document.getElementById('statusMessage');
const timeEl = document.getElementById('statusTime');
if (phaseEl) {
// Map phases to icons
const icons = {
"WRITER": "✍️", "ARCHITECT": "🏗️", "MARKETING": "🎨",
"ENRICHER": "🧠", "SYSTEM": "⚙️", "TIMING": "⏱️", "TRACKER": "🔎"
};
const icon = icons[data.progress.phase] || "🔄";
phaseEl.innerText = `${icon} ${data.progress.phase}`;
}
if (msgEl) msgEl.innerText = data.progress.message;
if (timeEl) {
const date = new Date(data.progress.timestamp * 1000);
timeEl.innerText = "Last update: " + date.toLocaleTimeString();
}
}
// If running, keep polling
if (data.status === 'running' || data.status === 'queued') {
if (!activeInterval) activeInterval = setInterval(fetchLog, 2000);
} else {
if (activeInterval) clearInterval(activeInterval);
activeInterval = null;
// Reload if we were polling (watched it finish) OR if page loaded as running but is now done
if (initialRunStatus === 'running' || initialRunStatus === 'queued') {
window.location.reload();
}
}
})
.catch(err => {
console.error("Polling failed:", err);
// Resume polling so the UI doesn't silently stop updating
if (!activeInterval) activeInterval = setInterval(fetchLog, 2000);
});
}
{% if active_run %}
fetchLog();
{% endif %}
function showRefiningModal() {
if (!document.getElementById('refineProgressModal')) {
const modalHtml = `
<div class="modal fade" id="refineProgressModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-body text-center p-4">
<div class="spinner-border text-warning mb-3" style="width: 3rem; height: 3rem;"></div>
<h4>Refining Bible...</h4><p class="text-muted">The AI is processing your changes.</p>
</div></div></div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
const modal = new bootstrap.Modal(document.getElementById('refineProgressModal'));
modal.show();
}
function submitRefineModal(event) {
event.preventDefault();
const form = event.target;
const btn = form.querySelector('button[type="submit"]');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Queueing...';
const instruction = form.querySelector('textarea[name="instruction"]').value;
fetch(`/project/{{ project.id }}/refine_bible`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ instruction: instruction, source: 'original' })
})
.then(res => res.json())
.then(data => {
if (data.task_id) {
const inputModalEl = document.getElementById('refineBibleModal');
const inputModal = bootstrap.Modal.getInstance(inputModalEl);
inputModal.hide();
showRefiningModal();
const pollInterval = setInterval(() => {
fetch(`/task_status/${data.task_id}`)
.then(r => {
if (!r.ok) throw new Error("Server error checking status");
return r.json();
})
.then(status => {
if (status.status === 'completed') {
clearInterval(pollInterval);
if (status.success) {
window.location.href = "/project/{{ project.id }}/bible_comparison";
} else {
alert("Refinement failed: " + (status.error || "Check logs for details."));
window.location.reload();
}
}
})
.catch(err => {
console.error("Polling error:", err);
}
);
}, 2000);
}
})
.catch(err => {
alert("Error: " + err);
btn.disabled = false;
btn.innerHTML = originalText;
});
}
{% if is_refining %}
document.addEventListener('DOMContentLoaded', function() {
showRefiningModal();
const pollInterval = setInterval(() => {
fetch("/project/{{ project.id }}/is_refining").then(r => r.json()).then(data => {
if (!data.is_refining) {
clearInterval(pollInterval);
window.location.href = "/project/{{ project.id }}/bible_comparison";
}
});
}, 2000);
});
{% endif %}
</script>
{% endblock %}