Expanded generation options. Multiple outfits support.
This commit is contained in:
40
templates/create.html
Normal file
40
templates/create.html
Normal 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 %}
|
||||
@@ -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
240
templates/edit.html
Normal 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 %}
|
||||
@@ -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
96
templates/settings.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user