- Remove ai_blueprint.md from git tracking (already gitignored) - web/app.py: Unify startup reset — all non-terminal states (running, queued, interrupted) are reset to 'failed' with per-job logging - web/routes/project.py: Add active_runs list to view_project() context - templates/project.html: Add Active Jobs card showing all running/queued jobs with status badge, start time, progress bar, and View Details link; Generate button and Stop buttons now driven by active_runs list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
743 lines
40 KiB
HTML
743 lines
40 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>
|
|
</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.
|
|
2. Click <span class="badge bg-success">Generate New Book</span>.
|
|
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>
|
|
|
|
<!-- 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 %} |