Adding files.
This commit is contained in:
104
templates/admin_dashboard.html
Normal file
104
templates/admin_dashboard.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2><i class="fas fa-shield-alt me-2 text-warning"></i>Admin Dashboard</h2>
|
||||
<p class="text-muted">System management and user administration.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="{{ url_for('admin_spend_report') }}" class="btn btn-outline-primary me-2"><i class="fas fa-chart-line me-2"></i>Spend Report</a>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary">Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- User Management -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-users me-2"></i>Users ({{ users|length }})</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>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>
|
||||
{% if u.is_admin %}<span class="badge bg-warning text-dark">Admin</span>{% else %}<span class="badge bg-secondary">User</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.id != current_user.id %}
|
||||
<form action="/admin/user/{{ u.id }}/delete" method="POST" onsubmit="return confirm('Delete user {{ u.username }} and ALL their projects? This cannot be undone.');">
|
||||
<a href="{{ url_for('impersonate_user', user_id=u.id) }}" class="btn btn-sm btn-outline-dark me-1" title="Impersonate User">
|
||||
<i class="fas fa-user-secret"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger" title="Delete User"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted small">Current</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Stats & Reset -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-chart-pie me-2"></i>System Stats</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<h3>{{ projects|length }}</h3>
|
||||
<span class="text-muted">Total Projects</span>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<h3>{{ users|length }}</h3>
|
||||
<span class="text-muted">Total Users</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-exclamation-triangle me-2"></i>Danger Zone</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Factory Reset:</strong> This will delete <strong>ALL</strong> projects, books, and users (except your admin account). It resets the system to a fresh state.</p>
|
||||
|
||||
<form action="/admin/reset" method="POST" onsubmit="return confirmReset()">
|
||||
<button type="submit" class="btn btn-danger w-100">
|
||||
<i class="fas fa-radiation me-2"></i>Perform Factory Reset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmReset() {
|
||||
return confirm("⚠️ WARNING: This will wipe ALL data on the server except your account.\n\nAre you absolutely sure?");
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
60
templates/admin_spend.html
Normal file
60
templates/admin_spend.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2><i class="fas fa-chart-line me-2 text-success"></i>Spend Report</h2>
|
||||
<p class="text-muted">Aggregate cost analysis per user.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-secondary">Back to Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Period Overview</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="?days=7" class="btn btn-outline-secondary {{ 'active' if days == 7 }}">7 Days</a>
|
||||
<a href="?days=30" class="btn btn-outline-secondary {{ 'active' if days == 30 }}">30 Days</a>
|
||||
<a href="?days=90" class="btn btn-outline-secondary {{ 'active' if days == 90 }}">90 Days</a>
|
||||
<a href="?days=0" class="btn btn-outline-secondary {{ 'active' if days == 0 }}">All Time</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-12">
|
||||
<h2 class="text-success fw-bold">${{ "%.4f"|format(total) }}</h2>
|
||||
<span class="text-muted">Total Spend ({{ 'Last ' ~ days ~ ' Days' if days > 0 else 'All Time' }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th class="text-center">Runs Generated</th>
|
||||
<th class="text-end">Total Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ row.username }}</td>
|
||||
<td class="text-center"><span class="badge bg-secondary">{{ row.runs }}</span></td>
|
||||
<td class="text-end font-monospace">${{ "%.4f"|format(row.cost) }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="text-center py-4 text-muted">No spending data found for this period.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
86
templates/base.html
Normal file
86
templates/base.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BookApp AI</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- FontAwesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
body { background-color: #f8f9fa; }
|
||||
.navbar-brand { font-weight: bold; letter-spacing: 1px; }
|
||||
.card { border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: transform 0.2s; }
|
||||
.card:hover { transform: translateY(-2px); }
|
||||
.btn-primary { background-color: #2c3e50; border-color: #2c3e50; }
|
||||
.btn-primary:hover { background-color: #1a252f; border-color: #1a252f; }
|
||||
.console-log {
|
||||
background-color: #1e1e1e; color: #00ff00;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
padding: 15px; border-radius: 5px;
|
||||
height: 400px; overflow-y: scroll;
|
||||
white-space: pre-wrap; font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% if session.get('original_admin_id') %}
|
||||
<div class="bg-danger text-white text-center py-2 shadow-sm" style="position: sticky; top: 0; z-index: 1050;">
|
||||
<strong><i class="fas fa-user-secret me-2"></i>Viewing site as {{ current_user.username }}</strong>
|
||||
<a href="{{ url_for('stop_impersonate') }}" class="btn btn-sm btn-light ms-3 text-danger fw-bold">Stop Impersonating</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/"><i class="fas fa-book-open me-2"></i>BookApp AI</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin %}
|
||||
<li class="nav-item me-3">
|
||||
<a class="nav-link text-warning" href="/admin"><i class="fas fa-shield-alt me-1"></i> Admin</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item me-3">
|
||||
<a class="nav-link" href="/personas"><i class="fas fa-users me-1"></i> Personas</a>
|
||||
</li>
|
||||
<li class="nav-item me-3">
|
||||
<span class="text-light small">
|
||||
<i class="fas fa-coins text-warning"></i> Spend: ${{ "%.2f"|format(current_user.total_spend) }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="text-light me-3">Hello, {{ current_user.username }}</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-sm btn-outline-light" href="/logout">Logout</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
98
templates/dashboard.html
Normal file
98
templates/dashboard.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2><i class="fas fa-layer-group me-2"></i>Your Projects</h2>
|
||||
<p class="text-muted">Manage your book series and generation tasks.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="/personas" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-users me-2"></i>Personas
|
||||
</a>
|
||||
<a href="/system/status" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-server me-2"></i>System Status
|
||||
</a>
|
||||
<button class="btn btn-outline-info me-2" onclick="optimizeModels()">
|
||||
<i class="fas fa-sync me-2"></i>Find New Models
|
||||
</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newProjectModal">
|
||||
<i class="fas fa-plus me-2"></i>New Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for p in projects %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ p.name }}</h5>
|
||||
<p class="card-text text-muted small">Created: {{ p.created_at.strftime('%Y-%m-%d') }}</p>
|
||||
<a href="/project/{{ p.id }}" class="btn btn-outline-primary stretched-link">Open Project</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12 text-center py-5">
|
||||
<h4 class="text-muted">No projects yet. Start writing!</h4>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- New Project Modal -->
|
||||
<div class="modal fade" id="newProjectModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<form class="modal-content" action="/project/setup" method="POST" onsubmit="showLoading(this)">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Project Wizard</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted">Describe your story idea, and the AI will suggest a title, genre, and structure for you.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Story Concept / Idea</label>
|
||||
<textarea name="concept" class="form-control" rows="6" placeholder="e.g. A detective in 1920s London discovers magic is real..." 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" id="analyzeBtn">Analyze & Next <i class="fas fa-arrow-right ms-2"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showLoading(form) {
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>AI Analyzing...';
|
||||
}
|
||||
|
||||
function optimizeModels() {
|
||||
if (!confirm("This will check for rate limits and re-select the best available models. Continue?")) return;
|
||||
|
||||
const btn = event.target.closest('button');
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Optimizing...';
|
||||
|
||||
fetch('/system/optimize_models', { method: 'POST' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.reload(); // Reload to see flashed message
|
||||
} else {
|
||||
alert('Optimization request failed.');
|
||||
}
|
||||
}).catch(err => {
|
||||
alert('Network error during optimization.');
|
||||
}).finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
27
templates/login.html
Normal file
27
templates/login.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-center mb-4">Login</h3>
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Sign In</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<small>Don't have an account? <a href="/register">Register here</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
121
templates/persona_edit.html
Normal file
121
templates/persona_edit.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">{% if name %}Edit Persona: {{ name }}{% else %}New Persona{% endif %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('save_persona') }}" method="POST">
|
||||
<input type="hidden" name="old_name" value="{{ name }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name / Pseudonym</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ persona.get('name', '') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Age</label>
|
||||
<input type="text" name="age" class="form-control" value="{{ persona.get('age', '') }}">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Gender</label>
|
||||
<input type="text" name="gender" class="form-control" value="{{ persona.get('gender', '') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Race / Ethnicity</label>
|
||||
<input type="text" name="race" class="form-control" value="{{ persona.get('race', '') }}">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Nationality</label>
|
||||
<input type="text" name="nationality" class="form-control" value="{{ persona.get('nationality', '') }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Primary Language</label>
|
||||
<input type="text" name="language" class="form-control" value="{{ persona.get('language', 'Standard English') }}">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Voice Keywords</label>
|
||||
<input type="text" name="voice_keywords" id="voiceKeywords" class="form-control" value="{{ persona.get('voice_keywords', '') }}" placeholder="e.g. Sarcastic, Fast-paced, Minimalist">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Style Inspirations</label>
|
||||
<input type="text" name="style_inspirations" id="styleInspirations" class="form-control" value="{{ persona.get('style_inspirations', '') }}" placeholder="e.g. Hemingway, Noir Fiction">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Bio / Writing Style</label>
|
||||
<textarea name="bio" id="bioField" class="form-control" rows="4">{{ persona.get('bio', '') }}</textarea>
|
||||
<div class="form-text">Describe the voice, tone, and stylistic quirks.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<label class="form-label mb-0">Sample Text (Manual)</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="analyzePersona()">
|
||||
<i class="fas fa-magic me-1"></i> Analyze & Auto-Fill
|
||||
</button>
|
||||
</div>
|
||||
<textarea name="sample_text" id="sampleText" class="form-control" rows="6">{{ persona.get('sample_text', '') }}</textarea>
|
||||
<div class="form-text">Paste a paragraph of text. The AI can analyze this to fill the fields above.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('list_personas') }}" class="btn btn-outline-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Save Persona</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function analyzePersona() {
|
||||
const btn = event.target;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Analyzing...';
|
||||
|
||||
const data = {
|
||||
name: document.querySelector('input[name="name"]').value,
|
||||
age: document.querySelector('input[name="age"]').value,
|
||||
gender: document.querySelector('input[name="gender"]').value,
|
||||
nationality: document.querySelector('input[name="nationality"]').value,
|
||||
sample_text: document.getElementById('sampleText').value
|
||||
};
|
||||
|
||||
fetch('/persona/analyze', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(resp => {
|
||||
if (resp.error) {
|
||||
alert("Error: " + resp.error);
|
||||
} else {
|
||||
if (resp.bio) document.getElementById('bioField').value = resp.bio;
|
||||
if (resp.voice_keywords) document.getElementById('voiceKeywords').value = resp.voice_keywords;
|
||||
if (resp.style_inspirations) document.getElementById('styleInspirations').value = resp.style_inspirations;
|
||||
}
|
||||
})
|
||||
.catch(err => alert("Request failed."))
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
32
templates/personas.html
Normal file
32
templates/personas.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-users me-2"></i>Author Personas</h2>
|
||||
<a href="{{ url_for('new_persona') }}" class="btn btn-primary"><i class="fas fa-plus me-2"></i>Create New Persona</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% for name, p in personas.items() %}
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ p.name }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{ p.age }} {{ p.gender }} | {{ p.nationality }}</h6>
|
||||
<p class="card-text small">{{ p.bio[:150] }}...</p>
|
||||
</div>
|
||||
<div class="card-footer bg-white border-top-0 d-flex justify-content-between">
|
||||
<a href="{{ url_for('edit_persona', name=name) }}" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||
<form action="{{ url_for('delete_persona', name=name) }}" method="POST" onsubmit="return confirm('Delete this persona?');">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">No personas found. Create one to define a writing voice.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
515
templates/project.html
Normal file
515
templates/project.html
Normal file
@@ -0,0 +1,515 @@
|
||||
{% 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>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editProjectModal"><i class="fas fa-edit"></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_run and active_run.status in ['running', 'queued'] %}disabled{% endif %}>
|
||||
<i class="fas fa-play me-2"></i>{{ 'Generating...' if runs and runs[0].status in ['running', 'queued'] else 'Generate New Book' }}
|
||||
</button>
|
||||
</form>
|
||||
{% if runs and runs[0].status in ['running', 'queued'] %}
|
||||
<form action="/run/{{ runs[0].id }}/stop" method="POST" class="d-inline ms-2">
|
||||
<button class="btn btn-danger shadow px-3 py-2" title="Stop/Cancel Run" onclick="return confirm('Are you sure you want to stop this job? If the server restarted, this will simply unlock the UI.')">
|
||||
<i class="fas fa-stop"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- LATEST RUN CARD -->
|
||||
<div class="card mb-4 border-0 shadow-sm">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<h5 class="card-title mb-3" id="statusMessage">Preparing environment...</h5>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: 100%"></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="/project/{{ project.id }}/run/{{ r.id }}" class="btn btn-sm btn-outline-primary">
|
||||
{{ 'View Active' if active_run and r.id == active_run.id 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">
|
||||
<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) %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i>Generated</span>
|
||||
{% 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">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</button>
|
||||
{% 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"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
{% 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 }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plot Summary / Instruction</label>
|
||||
<textarea name="instruction" class="form-control" rows="6">{{ book.manual_instruction }}</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">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Add Book Card -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WORLD BIBLE & LINKED SERIES -->
|
||||
<div class="row mb-4">
|
||||
<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>
|
||||
<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>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#importCharModal">
|
||||
<i class="fas fa-link me-1"></i> Link / Import Series
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary ms-1" data-bs-toggle="modal" data-bs-target="#refineBibleModal">
|
||||
<i class="fas fa-magic me-1"></i> Refine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
|
||||
<!-- 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" action="/project/{{ project.id }}/refine_bible" method="POST">
|
||||
<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' }};
|
||||
|
||||
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 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 page on completion to show download buttons
|
||||
if (data.status === 'completed' && !document.querySelector('.alert-success')) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{% if active_run %}
|
||||
fetchLog();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
92
templates/project_review.html
Normal file
92
templates/project_review.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0"><i class="fas fa-check-circle me-2"></i>Review Bible & Story Beats</h4>
|
||||
<a href="/project/{{ project.id }}" class="btn btn-light btn-sm fw-bold">Looks Good, Start Writing <i class="fas fa-arrow-right ms-1"></i></a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">The AI has generated your characters and plot structure. Review them below and refine if needed.</p>
|
||||
|
||||
<!-- Refinement Bar -->
|
||||
<form action="/project/{{ project.id }}/refine_bible" method="POST" class="mb-4" onsubmit="showLoading(this)">
|
||||
<div class="input-group shadow-sm">
|
||||
<span class="input-group-text bg-warning text-dark"><i class="fas fa-magic"></i></span>
|
||||
<input type="text" name="instruction" class="form-control" placeholder="AI Instruction: e.g. 'Change the ending of Book 1', 'Add a plot point about the ring', 'Make the tone darker'" required>
|
||||
<button type="submit" class="btn btn-warning">Refine with AI</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Characters Section -->
|
||||
<h5 class="text-primary border-bottom pb-2 mb-3">👥 Characters</h5>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 20%">Name</th>
|
||||
<th style="width: 20%">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>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="text-center text-muted">No characters generated.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Books & Beats Section -->
|
||||
<h5 class="text-primary border-bottom pb-2 mb-3">📚 Plot Structure</h5>
|
||||
{% for book in bible.books %}
|
||||
<div class="card mb-4 border-light bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-dark">
|
||||
<span class="badge bg-dark me-2">Book {{ book.book_number }}</span>
|
||||
{{ book.title }}
|
||||
</h5>
|
||||
<p class="card-text text-muted fst-italic border-start border-4 border-warning ps-3 mb-3">
|
||||
{{ book.manual_instruction }}
|
||||
</p>
|
||||
|
||||
<h6 class="fw-bold mt-3">Plot Beats:</h6>
|
||||
<ol class="list-group list-group-numbered">
|
||||
{% for beat in book.plot_beats %}
|
||||
<li class="list-group-item bg-white">{{ beat }}</li>
|
||||
{% else %}
|
||||
<li class="list-group-item text-muted">No plot beats generated.</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<a href="/project/{{ project.id }}" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-check me-2"></i>Finalize & Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showLoading(form) {
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Refining...';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
161
templates/project_setup.html
Normal file
161
templates/project_setup.html
Normal file
@@ -0,0 +1,161 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0"><i class="fas fa-magic me-2"></i>Project Setup</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">The AI has analyzed your concept. Review and edit the suggestions below before creating your project.</p>
|
||||
|
||||
<form method="POST" onsubmit="showLoading(this)">
|
||||
<input type="hidden" name="concept" value="{{ concept }}">
|
||||
<input type="hidden" name="persona_key" id="persona_key_input" value="{{ persona_key|default('') }}">
|
||||
|
||||
<!-- Refinement Bar -->
|
||||
<div class="input-group mb-4 shadow-sm">
|
||||
<span class="input-group-text bg-warning text-dark"><i class="fas fa-magic"></i></span>
|
||||
<input type="text" name="refine_instruction" class="form-control" placeholder="AI Instruction: e.g. 'Make it a trilogy', 'Change genre to Cyberpunk', 'Make the tone darker'">
|
||||
<button type="submit" formaction="/project/setup/refine" class="btn btn-warning">Refine with AI</button>
|
||||
</div>
|
||||
|
||||
<!-- Core Metadata -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-bold">Book Title</label>
|
||||
<input type="text" name="title" class="form-control form-control-lg" value="{{ s.title }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Genre</label>
|
||||
<input type="text" name="genre" class="form-control" value="{{ s.genre }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Target Audience</label>
|
||||
<input type="text" name="audience" class="form-control" value="{{ s.target_audience }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold text-primary">Select Author Persona (Optional)</label>
|
||||
<select class="form-select" onchange="applyPersona(this)">
|
||||
<option value="">-- Select a Persona to Auto-fill --</option>
|
||||
{% for key, p in personas.items() %}
|
||||
<option value="{{ key }}" data-name="{{ p.name }}" data-bio="{{ p.bio }}">{{ key }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Author Name</label>
|
||||
<input type="text" name="author" class="form-control" value="{{ current_user.username }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Author Bio / Persona</label>
|
||||
<textarea name="author_bio" class="form-control" rows="2">{{ s.author_bio }}</textarea>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Structure -->
|
||||
<h5 class="text-primary mb-3">Structure & Length</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12 mb-2">
|
||||
<label class="form-label">Length Category</label>
|
||||
<select name="length_category" class="form-select">
|
||||
{% for key, val in lengths.items() %}
|
||||
<option value="{{ key }}" {% if key == s.length_category %}selected{% endif %}>
|
||||
{{ val.label }} ({{ val.words }} words)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Est. Chapters</label>
|
||||
<input type="number" name="chapters" class="form-control" value="{{ s.estimated_chapters }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Est. Word Count</label>
|
||||
<input type="text" name="words" class="form-control" value="{{ s.estimated_word_count }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="include_prologue" {% if s.include_prologue %}checked{% endif %}>
|
||||
<label class="form-check-label">Include Prologue</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="include_epilogue" {% if s.include_epilogue %}checked{% endif %}>
|
||||
<label class="form-check-label">Include Epilogue</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="is_series" {% if s.is_series %}checked{% endif %}>
|
||||
<label class="form-check-label">Is Series</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-label ms-2 small">Book Count:</label>
|
||||
<input type="number" name="series_count" class="form-control d-inline-block py-0 px-2" style="width: 60px; height: 25px;" value="{{ s.series_count|default(1) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Style -->
|
||||
<h5 class="text-primary mb-3">Style & Tone</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Tone</label><input type="text" name="tone" class="form-control" value="{{ s.tone }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">POV Style</label><input type="text" name="pov_style" class="form-control" value="{{ s.pov_style }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Time Period</label><input type="text" name="time_period" class="form-control" value="{{ s.time_period }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Spice Level</label><input type="text" name="spice" class="form-control" value="{{ s.spice }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Violence</label><input type="text" name="violence" class="form-control" value="{{ s.violence }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Narrative Tense</label><input type="text" name="narrative_tense" class="form-control" value="{{ s.narrative_tense }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Language Style</label><input type="text" name="language_style" class="form-control" value="{{ s.language_style }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Dialogue Style</label><input type="text" name="dialogue_style" class="form-control" value="{{ s.dialogue_style }}"></div>
|
||||
<div class="col-md-6 mb-2"><label class="form-label">Page Orientation</label>
|
||||
<select name="page_orientation" class="form-select"><option value="Portrait" {% if s.page_orientation == 'Portrait' %}selected{% endif %}>Portrait</option><option value="Landscape" {% if s.page_orientation == 'Landscape' %}selected{% endif %}>Landscape</option><option value="Square" {% if s.page_orientation == 'Square' %}selected{% endif %}>Square</option></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Tropes (comma separated)</label>
|
||||
<input type="text" name="tropes" class="form-control" value="{{ s.tropes|join(', ') }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Formatting Rules (comma separated)</label>
|
||||
<input type="text" name="formatting_rules" class="form-control" value="{{ s.formatting_rules|join(', ') }}">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" formaction="/project/create" class="btn btn-success btn-lg">
|
||||
<i class="fas fa-check me-2"></i>Create Project & Generate Bible
|
||||
</button>
|
||||
<a href="/" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showLoading(form) {
|
||||
// We don't disable buttons here because formaction needs the specific button click
|
||||
// But we can show a global spinner or change cursor
|
||||
document.body.style.cursor = 'wait';
|
||||
}
|
||||
|
||||
function applyPersona(select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
if (opt.value) {
|
||||
document.querySelector('input[name="author"]').value = opt.dataset.name || opt.value;
|
||||
document.querySelector('textarea[name="author_bio"]').value = opt.dataset.bio || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
27
templates/register.html
Normal file
27
templates/register.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-center mb-4">Register</h3>
|
||||
<form method="POST" action="/register">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success">Create Account</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<small>Already have an account? <a href="/login">Login here</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
298
templates/run_details.html
Normal file
298
templates/run_details.html
Normal file
@@ -0,0 +1,298 @@
|
||||
{% 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>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column: Cover Art -->
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-image me-2"></i>Cover Art</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
{% if has_cover %}
|
||||
<img src="{{ url_for('download_artifact', run_id=run.id, file='cover.png') }}" class="img-fluid rounded shadow-sm mb-3" alt="Book Cover">
|
||||
{% else %}
|
||||
<div class="alert alert-secondary py-5">
|
||||
<i class="fas fa-image fa-3x mb-3"></i><br>
|
||||
No cover generated yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<form action="{{ url_for('regenerate_artifacts', run_id=run.id) }}" method="POST">
|
||||
<label class="form-label text-start w-100 small fw-bold">Regenerate Art & Files</label>
|
||||
<textarea name="feedback" class="form-control mb-2" rows="2" placeholder="Feedback (e.g. 'Make the font larger', 'Use a darker theme')..."></textarea>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-sync me-2"></i>Regenerate
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Blurb & Stats -->
|
||||
<div class="col-md-8 mb-4">
|
||||
<!-- Blurb -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-align-left me-2"></i>Blurb</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if blurb %}
|
||||
<p class="card-text" style="white-space: pre-wrap;">{{ blurb }}</p>
|
||||
{% else %}
|
||||
<p class="text-muted fst-italic">Blurb not generated yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<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 %}
|
||||
124
templates/system_status.html
Normal file
124
templates/system_status.html
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h2><i class="fas fa-server me-2"></i>System Status</h2>
|
||||
<p class="text-muted">AI Model Health, Selection Reasoning, and Availability.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary me-2">Back to Dashboard</a>
|
||||
<form action="{{ url_for('optimize_models') }}" method="POST" class="d-inline" onsubmit="return confirm('This will re-analyze all available models. Continue?');">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-sync me-2"></i>Refresh & Optimize
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-robot me-2"></i>AI Model Selection</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 15%">Role</th>
|
||||
<th style="width: 25%">Selected Model</th>
|
||||
<th>Selection Reasoning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if models %}
|
||||
{% for role, info in models.items() %}
|
||||
{% if role != 'ranking' %}
|
||||
<tr>
|
||||
<td class="fw-bold text-uppercase">{{ role }}</td>
|
||||
<td>
|
||||
{% if info is mapping %}
|
||||
<span class="badge bg-info text-dark">{{ info.model }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ info }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small text-muted">
|
||||
{% if info is mapping %}
|
||||
{{ info.reason }}
|
||||
{% else %}
|
||||
<em>Legacy format. Please refresh models.</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-muted">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>No model configuration found.
|
||||
<br>Click <strong>Refresh & Optimize</strong> to scan available Gemini models.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ranked Models -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-list-ol me-2"></i>All Models Ranked</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 10%">Rank</th>
|
||||
<th style="width: 30%">Model Name</th>
|
||||
<th>Reasoning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if models and models.ranking %}
|
||||
{% for item in models.ranking %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ loop.index }}</td>
|
||||
<td><span class="badge bg-secondary">{{ item.model }}</span></td>
|
||||
<td class="small text-muted">{{ item.reason }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-muted">
|
||||
No ranking data available. Refresh models to generate.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cache Info -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-clock me-2"></i>Cache Status</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">
|
||||
<strong>Last Scan:</strong>
|
||||
{% if cache and cache.timestamp %}
|
||||
{{ datetime.fromtimestamp(cache.timestamp).strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-muted small mb-0">Model selection is cached for 24 hours to save API calls.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user