Expanded generation options. Multiple outfits support.

This commit is contained in:
Aodhan Collins
2026-02-19 00:40:29 +00:00
parent 1e7f252cf9
commit 5aede18ad5
65 changed files with 2086 additions and 477 deletions

40
templates/create.html Normal file
View File

@@ -0,0 +1,40 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-success text-white">Create New Character</div>
<div class="card-body">
<form action="{{ url_for('create_character') }}" method="post">
<div class="mb-3">
<label for="name" class="form-label">Character Name</label>
<input type="text" class="form-control" id="name" name="name" placeholder="e.g. Cyberpunk Ninja" required>
</div>
<div class="mb-3">
<label for="filename" class="form-label">Filename (Slug)</label>
<input type="text" class="form-control" id="filename" name="filename" placeholder="e.g. cyberpunk_ninja" required>
<div class="form-text">Used for the JSON file and URL. No spaces or special characters.</div>
</div>
<div class="mb-3">
<label for="prompt" class="form-label">Description / Concept</label>
<textarea class="form-control" id="prompt" name="prompt" rows="5" placeholder="Describe the character's appearance, clothing, style, and personality. The AI will generate the full profile based on this." required></textarea>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Once created, the system will automatically attempt to generate a cover image using the new profile.
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success btn-lg">Create & Generate</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -79,13 +79,77 @@
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>{{ character.name }}</h1>
<div>
<h1 class="mb-0">{{ character.name }}</h1>
<a href="{{ url_for('edit_character', slug=character.slug) }}" class="btn btn-sm btn-link text-decoration-none">Edit Profile</a>
</div>
<a href="/" class="btn btn-outline-secondary">Back to Gallery</a>
</div>
<!-- Outfit Switcher -->
{% set outfits = character.get_available_outfits() %}
{% if outfits|length > 1 %}
<div class="card mb-4 border-primary">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<span><i class="bi bi-shirt"></i> Active Outfit</span>
<span class="badge bg-light text-primary">{{ character.active_outfit or 'default' }}</span>
</div>
<div class="card-body">
<form action="{{ url_for('switch_outfit', slug=character.slug) }}" method="post" class="row g-2">
<div class="col-auto flex-grow-1">
<select name="outfit" class="form-select" id="outfit-select">
{% for outfit in outfits %}
<option value="{{ outfit }}" {% if outfit == character.active_outfit %}selected{% endif %}>
{{ outfit }}
</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">Switch</button>
</div>
</form>
</div>
</div>
{% endif %}
<form id="generate-form" action="{{ url_for('generate_image', slug=character.slug) }}" method="post">
{% for section, details in character.data.items() %}
{% if section not in ['character_id', 'tags', 'name'] and details is mapping %}
{% if section == 'wardrobe' %}
{# Special handling for wardrobe - show active outfit #}
{% set active_wardrobe = character.get_active_wardrobe() %}
<div class="card mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<strong>
Wardrobe
{% if outfits|length > 1 %}
<span class="badge bg-secondary ms-2">{{ character.active_outfit or 'default' }}</span>
{% endif %}
</strong>
{% if outfits|length > 1 %}
<a href="{{ url_for('edit_character', slug=character.slug) }}#outfits" class="btn btn-sm btn-outline-secondary">Manage Outfits</a>
{% endif %}
</div>
<div class="card-body">
<dl class="row mb-0">
{% for key, value in active_wardrobe.items() %}
<dt class="col-sm-4 text-capitalize">
<input class="form-check-input me-1" type="checkbox" name="include_field" value="wardrobe::{{ key }}"
{% if preferences is not none %}
{% if 'wardrobe::' + key in preferences %}checked{% endif %}
{% elif character.default_fields is not none %}
{% if 'wardrobe::' + key in character.default_fields %}checked{% endif %}
{% else %}
{% if value %}checked{% endif %}
{% endif %}>
{{ key.replace('_', ' ') }}
</dt>
<dd class="col-sm-8">{{ value if value else '--' }}</dd>
{% endfor %}
</dl>
</div>
</div>
{% elif section not in ['character_id', 'tags', 'name'] and details is mapping %}
<div class="card mb-4">
<div class="card-header bg-light text-capitalize"><strong>{{ section.replace('_', ' ') }}</strong></div>
<div class="card-body">

240
templates/edit.html Normal file
View File

@@ -0,0 +1,240 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Edit Profile: {{ character.name }}</h1>
<a href="{{ url_for('detail', slug=character.slug) }}" class="btn btn-outline-secondary">Cancel</a>
</div>
<form action="{{ url_for('edit_character', slug=character.slug) }}" method="post" id="main-form">
<div class="row">
<div class="col-md-8">
<!-- Basic Info -->
<div class="card mb-4">
<div class="card-header bg-dark text-white">Basic Information</div>
<div class="card-body">
<div class="mb-3">
<label for="character_name" class="form-label">Display Name</label>
<input type="text" class="form-control" id="character_name" name="character_name" value="{{ character.name }}" required>
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags (comma separated)</label>
<input type="text" class="form-control" id="tags" name="tags" value="{{ character.data.tags | join(', ') }}">
</div>
</div>
</div>
<!-- LoRA -->
<div class="card mb-4">
<div class="card-header bg-info text-white">LoRA Settings</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<label for="lora_lora_name" class="form-label">LoRA Name</label>
<select class="form-select" id="lora_lora_name" name="lora_lora_name">
<option value="">None</option>
{% for lora in loras %}
<option value="{{ lora }}" {% if character.data.lora.lora_name == lora %}selected{% endif %}>{{ lora }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label for="lora_lora_weight" class="form-label">Weight</label>
<input type="number" step="0.01" class="form-control" id="lora_lora_weight" name="lora_lora_weight" value="{{ character.data.lora.lora_weight }}">
</div>
</div>
<div class="mt-3">
<label for="lora_lora_triggers" class="form-label">Triggers</label>
<input type="text" class="form-control" id="lora_lora_triggers" name="lora_lora_triggers" value="{{ character.data.lora.lora_triggers }}">
</div>
</div>
</div>
<!-- Identity Section -->
{% if character.data.identity %}
<div class="card mb-4">
<div class="card-header bg-light"><strong>Identity</strong></div>
<div class="card-body">
{% for key, value in character.data.identity.items() %}
<div class="mb-3">
<label for="identity_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
<input type="text" class="form-control" id="identity_{{ key }}" name="identity_{{ key }}" value="{{ value }}">
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Defaults Section -->
{% if character.data.defaults %}
<div class="card mb-4">
<div class="card-header bg-light"><strong>Defaults</strong></div>
<div class="card-body">
{% for key, value in character.data.defaults.items() %}
<div class="mb-3">
<label for="defaults_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
<input type="text" class="form-control" id="defaults_{{ key }}" name="defaults_{{ key }}" value="{{ value }}">
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Wardrobe Section - Show all outfits with tabs -->
<div class="card mb-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<strong>Wardrobe</strong>
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addOutfitModal">
<i class="bi bi-plus-lg"></i> Add Outfit
</button>
</div>
<div class="card-body">
{% set wardrobe_data = character.data.wardrobe %}
{% set outfits = character.get_available_outfits() %}
{% if wardrobe_data.default is defined and wardrobe_data.default is mapping %}
{# New nested format - show tabs for each outfit #}
<ul class="nav nav-tabs mb-3" id="wardrobeTabs" role="tablist">
{% for outfit_name in outfits %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if loop.first %}active{% endif %}" id="outfit-{{ outfit_name }}-tab" data-bs-toggle="tab" data-bs-target="#outfit-{{ outfit_name }}" type="button" role="tab">
{{ outfit_name }}
{% if outfit_name == character.active_outfit %}
<span class="badge bg-primary ms-1">Active</span>
{% endif %}
</button>
</li>
{% endfor %}
</ul>
<div class="tab-content" id="wardrobeTabContent">
{% for outfit_name in outfits %}
<div class="tab-pane fade {% if loop.first %}show active{% endif %}" id="outfit-{{ outfit_name }}" role="tabpanel">
<div class="d-flex justify-content-end mb-2">
{% if outfit_name != 'default' %}
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#renameOutfitModal" data-outfit="{{ outfit_name }}">Rename</button>
<form action="{{ url_for('delete_outfit', slug=character.slug) }}" method="post" class="d-inline" onsubmit="return confirm('Delete outfit \'{{ outfit_name }}\'?');">
<input type="hidden" name="outfit" value="{{ outfit_name }}">
<button type="submit" class="btn btn-outline-danger">Delete</button>
</form>
</div>
{% endif %}
</div>
{% for key, value in wardrobe_data[outfit_name].items() %}
<div class="mb-3">
<label for="wardrobe_{{ outfit_name }}_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
<input type="text" class="form-control" id="wardrobe_{{ outfit_name }}_{{ key }}" name="wardrobe_{{ outfit_name }}_{{ key }}" value="{{ value }}">
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% else %}
{# Legacy flat format #}
{% for key, value in wardrobe_data.items() %}
<div class="mb-3">
<label for="wardrobe_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
<input type="text" class="form-control" id="wardrobe_{{ key }}" name="wardrobe_{{ key }}" value="{{ value }}">
</div>
{% endfor %}
{% endif %}
</div>
</div>
<!-- Styles Section -->
{% if character.data.styles %}
<div class="card mb-4">
<div class="card-header bg-light"><strong>Styles</strong></div>
<div class="card-body">
{% for key, value in character.data.styles.items() %}
<div class="mb-3">
<label for="styles_{{ key }}" class="form-label text-capitalize">{{ key.replace('_', ' ') }}</label>
<input type="text" class="form-control" id="styles_{{ key }}" name="styles_{{ key }}" value="{{ value }}">
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="mb-5">
<button type="submit" class="btn btn-primary btn-lg w-100">Save Changes to JSON</button>
</div>
</div>
<div class="col-md-4">
<div class="sticky-top" style="top: 20px;">
<div class="card">
<div class="card-header bg-warning text-dark">Notice</div>
<div class="card-body small">
<p>Saving changes here will overwrite the original JSON file in the <code>characters/</code> folder.</p>
<p>Character ID (<code>{{ character.character_id }}</code>) cannot be changed via the GUI to maintain file and URL consistency.</p>
<hr>
<p><strong>Outfits:</strong> Add multiple outfits using the "Add Outfit" button in the Wardrobe section. Switch between them on the character detail page.</p>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Add Outfit Modal -->
<div class="modal fade" id="addOutfitModal" tabindex="-1" aria-labelledby="addOutfitModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('add_outfit', slug=character.slug) }}" method="post">
<div class="modal-header">
<h5 class="modal-title" id="addOutfitModalLabel">Add New Outfit</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newOutfitName" class="form-label">Outfit Name</label>
<input type="text" class="form-control" id="newOutfitName" name="outfit_name" placeholder="e.g., casual, formal, swimwear">
<div class="form-text">Name will be converted to lowercase with underscores.</div>
</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">Add Outfit</button>
</div>
</form>
</div>
</div>
</div>
<!-- Rename Outfit Modal -->
<div class="modal fade" id="renameOutfitModal" tabindex="-1" aria-labelledby="renameOutfitModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('rename_outfit', slug=character.slug) }}" method="post">
<div class="modal-header">
<h5 class="modal-title" id="renameOutfitModalLabel">Rename Outfit</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="old_name" id="renameOldName">
<div class="mb-3">
<label for="renameNewName" class="form-label">New Name</label>
<input type="text" class="form-control" id="renameNewName" name="new_name" placeholder="Enter new outfit name">
</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">Rename</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Populate rename modal with current outfit name
document.getElementById('renameOutfitModal').addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var outfitName = button.getAttribute('data-outfit');
document.getElementById('renameOldName').value = outfitName;
document.getElementById('renameNewName').value = outfitName;
});
</script>
{% endblock %}

View File

@@ -18,7 +18,9 @@
<div class="container">
<a class="navbar-brand" href="/">Character Browser</a>
<div class="d-flex">
<a href="/create" class="btn btn-outline-success me-2">Create Character</a>
<a href="/generator" class="btn btn-outline-light me-2">Generator</a>
<a href="/settings" class="btn btn-outline-light">Settings</a>
</div>
</div>
</nav>

96
templates/settings.html Normal file
View File

@@ -0,0 +1,96 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-dark text-white">Application Settings</div>
<div class="card-body">
<form method="post">
<h5 class="card-title mb-3">LLM Configuration (OpenRouter)</h5>
<div class="mb-3">
<label for="api_key" class="form-label">OpenRouter API Key</label>
<div class="input-group">
<input type="password" class="form-control" id="api_key" name="api_key" value="{{ settings.openrouter_api_key or '' }}">
<button class="btn btn-outline-secondary" type="button" id="connect-btn">Connect & Load Models</button>
</div>
<div class="form-text">Required for AI text generation features.</div>
</div>
<div class="mb-3">
<label for="model" class="form-label">Model Selection</label>
<select class="form-select" id="model" name="model">
<option value="{{ settings.openrouter_model }}" selected>{{ settings.openrouter_model }}</option>
</select>
<div class="form-text">Click "Connect" above to load the latest available models.</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Save Settings</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const connectBtn = document.getElementById('connect-btn');
const apiKeyInput = document.getElementById('api_key');
const modelSelect = document.getElementById('model');
const currentModel = "{{ settings.openrouter_model }}";
connectBtn.addEventListener('click', async () => {
const apiKey = apiKeyInput.value;
if (!apiKey) {
alert('Please enter an API Key first.');
return;
}
connectBtn.disabled = true;
connectBtn.textContent = 'Connecting...';
try {
const formData = new FormData();
formData.append('api_key', apiKey);
const response = await fetch('/get_openrouter_models', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert('Error: ' + data.error);
} else {
// Clear and populate dropdown
modelSelect.innerHTML = '';
data.models.sort((a, b) => a.name.localeCompare(b.name)).forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
if (model.id === currentModel) {
option.selected = true;
}
modelSelect.appendChild(option);
});
alert('Model list loaded successfully!');
}
} catch (err) {
console.error(err);
alert('Failed to connect to OpenRouter.');
} finally {
connectBtn.disabled = false;
connectBtn.textContent = 'Connect & Load Models';
}
});
});
</script>
{% endblock %}