Replaced monolithic modules/ package with a clean architecture:
- core/ config.py, utils.py
- ai/ models.py (ResilientModel), setup.py (init_models)
- story/ planner.py, writer.py, editor.py, style_persona.py, bible_tracker.py
- marketing/ cover.py, blurb.py, fonts.py, assets.py
- export/ exporter.py
- web/ app.py (Flask factory), db.py, helpers.py, tasks.py, routes/{auth,project,run,persona,admin}.py
- cli/ engine.py (run_generation), wizard.py (BookWizard)
Flask routes split into 5 Blueprints; all templates updated with blueprint-
prefixed url_for() calls. Dockerfile and docker-compose updated to use
web.app entry point and new package paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
6.1 KiB
HTML
121 lines
6.1 KiB
HTML
{% 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('persona.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('persona.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 %} |