Files
bookapp/templates/run_details.html
2026-02-03 13:49:49 -05:00

308 lines
15 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2><i class="fas fa-book me-2"></i>Run #{{ run.id }}</h2>
<p class="text-muted mb-0">Project: <a href="{{ url_for('view_project', id=run.project_id) }}">{{ run.project.name }}</a></p>
</div>
<div>
<button class="btn btn-outline-primary me-2" type="button" data-bs-toggle="collapse" data-bs-target="#bibleCollapse" aria-expanded="false" aria-controls="bibleCollapse">
<i class="fas fa-scroll me-2"></i>Show Bible
</button>
<a href="{{ url_for('view_project', id=run.project_id) }}" class="btn btn-outline-secondary">Back to Project</a>
</div>
</div>
<!-- Collapsible Bible Viewer -->
<div class="collapse mb-4" id="bibleCollapse">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-book-open me-2"></i>Project Bible</h5>
</div>
<div class="card-body">
{% if bible %}
<ul class="nav nav-tabs" id="bibleTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="meta-tab" data-bs-toggle="tab" data-bs-target="#meta" type="button" role="tab">Metadata</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="chars-tab" data-bs-toggle="tab" data-bs-target="#chars" type="button" role="tab">Characters</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="plot-tab" data-bs-toggle="tab" data-bs-target="#plot" type="button" role="tab">Plot</button>
</li>
</ul>
<div class="tab-content p-3 border border-top-0">
<!-- Metadata Tab -->
<div class="tab-pane fade show active" id="meta" role="tabpanel">
<dl class="row mb-0">
{% for k, v in bible.project_metadata.items() %}
{% if k not in ['style', 'length_settings', 'author_details'] %}
<dt class="col-sm-3 text-capitalize">{{ k.replace('_', ' ') }}</dt>
<dd class="col-sm-9">{{ v }}</dd>
{% endif %}
{% endfor %}
{% if bible.project_metadata.style %}
<dt class="col-sm-12 mt-3 border-bottom pb-2">Style Settings</dt>
{% for k, v in bible.project_metadata.style.items() %}
<dt class="col-sm-3 text-capitalize mt-2">{{ k.replace('_', ' ') }}</dt>
<dd class="col-sm-9 mt-2">{{ v|join(', ') if v is sequence and v is not string else v }}</dd>
{% endfor %}
{% endif %}
</dl>
</div>
<!-- Characters Tab -->
<div class="tab-pane fade" id="chars" role="tabpanel">
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead class="table-light"><tr><th>Name</th><th>Role</th><th>Description</th></tr></thead>
<tbody>
{% for c in bible.characters %}
<tr>
<td class="fw-bold">{{ c.name }}</td>
<td><span class="badge bg-secondary">{{ c.role }}</span></td>
<td class="small">{{ c.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Plot Tab -->
<div class="tab-pane fade" id="plot" role="tabpanel">
{% for book in bible.books %}
<div class="mb-3">
<h6 class="fw-bold text-primary">Book {{ book.book_number }}: {{ book.title }}</h6>
<p class="fst-italic small text-muted">{{ book.manual_instruction }}</p>
<ol class="list-group list-group-numbered">
{% for beat in book.plot_beats %}
<li class="list-group-item list-group-item-action border-0 py-1">{{ beat }}</li>
{% endfor %}
</ol>
</div>
{% endfor %}
</div>
</div>
{% else %}
<div class="alert alert-warning">Bible data not found for this project.</div>
{% endif %}
</div>
</div>
</div>
<!-- Status Bar -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span class="fw-bold" id="status-text">Status: {{ run.status|title }}</span>
<span class="text-muted" id="run-duration">{{ run.duration() }}</span>
</div>
<div class="progress" style="height: 20px;">
<div id="status-bar" class="progress-bar {% if run.status == 'running' %}progress-bar-striped progress-bar-animated{% elif run.status == 'failed' %}bg-danger{% else %}bg-success{% endif %}"
role="progressbar" style="width: {% if run.status == 'completed' %}100%{% elif run.status == 'running' %}100%{% else %}5%{% endif %}">
</div>
</div>
</div>
</div>
<!-- Generated Books in this Run -->
{% for book in books %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-book me-2"></i>{{ book.folder }}</h5>
</div>
<div class="card-body">
<div class="row">
<!-- Left Column: Cover Art -->
<div class="col-md-4 mb-3">
<div class="text-center">
{% if book.cover %}
<img src="{{ url_for('download_artifact', run_id=run.id, file=book.cover) }}" class="img-fluid rounded shadow-sm mb-3" alt="Book Cover" style="max-height: 400px;">
{% else %}
<div class="alert alert-secondary py-5">
<i class="fas fa-image fa-3x mb-3"></i><br>No cover.
</div>
{% endif %}
{% if loop.first %}
<form action="{{ url_for('regenerate_artifacts', run_id=run.id) }}" method="POST" class="mt-2">
<textarea name="feedback" class="form-control mb-2 form-control-sm" rows="1" placeholder="Cover Feedback..."></textarea>
<button type="submit" class="btn btn-sm btn-outline-primary w-100">
<i class="fas fa-sync me-2"></i>Regenerate All
</button>
</form>
{% endif %}
</div>
</div>
<!-- Right Column: Blurb -->
<div class="col-md-8">
<h6 class="fw-bold">Back Cover Blurb</h6>
<div class="p-3 bg-light rounded mb-3">
{% if book.blurb %}
<p class="mb-0" style="white-space: pre-wrap;">{{ book.blurb }}</p>
{% else %}
<p class="text-muted fst-italic mb-0">No blurb generated.</p>
{% endif %}
</div>
<h6 class="fw-bold">Artifacts</h6>
<div class="d-flex flex-wrap gap-2">
{% for art in book.artifacts %}
<a href="{{ url_for('download_artifact', run_id=run.id, file=art.path) }}" class="btn btn-sm btn-outline-success">
<i class="fas fa-download me-1"></i> {{ art.name }}
</a>
{% else %}
<span class="text-muted small">No files found.</span>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
<div class="row mb-4">
<div class="col-6">
<div class="card shadow-sm text-center">
<div class="card-body">
<h6 class="text-muted">Total Cost</h6>
<h3 class="text-success" id="run-cost">${{ "%.4f"|format(run.cost) }}</h3>
</div>
</div>
</div>
<div class="col-6">
<div class="card shadow-sm text-center">
<div class="card-body">
<h6 class="text-muted">Artifacts</h6>
<h3>
{% if has_cover %}<i class="fas fa-check text-success me-2"></i>{% endif %}
{% if blurb %}<i class="fas fa-check text-success"></i>{% endif %}
</h3>
</div>
</div>
</div>
</div>
<!-- Tracking & Warnings -->
{% if tracking and (tracking.content_warnings or tracking.characters) %}
<div class="card shadow-sm mb-4 border-warning">
<div class="card-header bg-warning-subtle">
<h5 class="mb-0 text-warning-emphasis"><i class="fas fa-exclamation-triangle me-2"></i>Story Tracking & Warnings</h5>
</div>
<div class="card-body">
{% if tracking.content_warnings %}
<div class="mb-3">
<h6 class="fw-bold">Content Warnings Detected:</h6>
<div>
{% for w in tracking.content_warnings %}
<span class="badge bg-danger me-1 mb-1">{{ w }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if tracking.characters %}
{# Check if any character actually has major events to display #}
{% set has_events = namespace(value=false) %}
{% for name, data in tracking.characters.items() %}
{% if data.major_events %}{% set has_events.value = true %}{% endif %}
{% endfor %}
{% if has_events.value %}
<h6 class="fw-bold">Major Character Events:</h6>
<div class="accordion" id="charEventsAcc">
{% for name, data in tracking.characters.items() %}
{% if data.major_events %}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#ce-{{ loop.index }}">
{{ name }}
</button>
</h2>
<div id="ce-{{ loop.index }}" class="accordion-collapse collapse" data-bs-parent="#charEventsAcc">
<div class="accordion-body small py-2">
<ul class="mb-0 ps-3">
{% for evt in data.major_events %}
<li>{{ evt }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
<!-- Collapsible Log -->
<div class="card shadow-sm">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#logCollapse">
<h5 class="mb-0"><i class="fas fa-terminal me-2"></i>System Log</h5>
<span class="badge bg-secondary" id="log-badge">Click to Toggle</span>
</div>
<div id="logCollapse" class="collapse {% if run.status == 'running' %}show{% endif %}">
<div class="card-body bg-dark p-0">
<pre id="console-log" class="console-log m-0 p-3" style="color: #00ff00; background-color: #1e1e1e; height: 400px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">{{ log_content or "Waiting for logs..." }}</pre>
</div>
</div>
</div>
</div>
</div>
<script>
const runId = {{ run.id }};
const consoleEl = document.getElementById('console-log');
const statusText = document.getElementById('status-text');
const statusBar = document.getElementById('status-bar');
const costEl = document.getElementById('run-cost');
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);
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 = "100%";
} else if (data.status === 'failed') {
statusBar.className = "progress-bar bg-danger";
statusBar.style.width = "100%";
} else {
statusBar.className = "progress-bar bg-success";
statusBar.style.width = "100%";
}
// Update Log (only if changed to avoid scroll jitter)
if (consoleEl.innerText !== data.log) {
const isScrolledToBottom = consoleEl.scrollHeight - consoleEl.clientHeight <= consoleEl.scrollTop + 50;
consoleEl.innerText = data.log;
if (isScrolledToBottom) {
consoleEl.scrollTop = consoleEl.scrollHeight;
}
}
// Poll if running
if (data.status === 'running' || data.status === 'queued') {
setTimeout(updateLog, 2000);
}
})
.catch(err => console.error(err));
}
// Start polling
updateLog();
</script>
{% endblock %}