Add admin features, user profile management, and generation capabilities
- Implemented admin dashboard with user management features including role assignment and deletion. - Added user profile page for updating email and password. - Created separate routes and templates for text, image, and video generation with appropriate forms. - Enhanced navigation with dropdown for generation options and loading overlay for better user experience. - Introduced comprehensive error handling and user feedback through alerts. - Updated styles for improved UI consistency and responsiveness. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>Admin Dashboard</h1>
|
||||
|
||||
{% if stats %}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Total users</div>
|
||||
<div class="stat-value">{{ stats.get('total_users', 0) }}</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Active tokens</div>
|
||||
<div class="stat-value">{{ stats.get('active_refresh_tokens', 0) }}</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Admins</div>
|
||||
<div class="stat-value">{{ stats.get('admin_users', 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="section-title">Users</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>
|
||||
<span class="role-badge role-{{ u.role }}">{{ u.role }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<!-- Role toggle -->
|
||||
<form method="post" action="{{ url_for('admin_set_role', user_id=u.id) }}">
|
||||
<input type="hidden" name="role"
|
||||
value="{{ 'user' if u.role == 'admin' else 'admin' }}">
|
||||
<button type="submit" class="btn btn-sm">
|
||||
Make {{ 'user' if u.role == 'admin' else 'admin' }}
|
||||
</button>
|
||||
</form>
|
||||
<!-- Delete -->
|
||||
{% if u.id != session.get('user_id') %}
|
||||
<form method="post" action="{{ url_for('admin_delete_user', user_id=u.id) }}"
|
||||
onsubmit="return confirm('Delete {{ u.email }}?')">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="text-muted">No users found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,36 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}AI Allucanget{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="{{ url_for('index') }}" class="brand">AI Allucanget</a>
|
||||
<div class="nav-links">
|
||||
{% if session.get('access_token') %}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}AI Allucanget{% endblock %}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='style.css') }}"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="{{ url_for('index') }}" class="brand">AI Allucanget</a>
|
||||
|
||||
<button class="hamburger" aria-label="Open menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
|
||||
<div class="nav-links">
|
||||
{% if session.get('access_token') %}
|
||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
<a href="{{ url_for('generate') }}">Generate</a>
|
||||
|
||||
<div class="nav-dropdown">
|
||||
<a href="{{ url_for('generate_text') }}">Generate ▾</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a href="{{ url_for('generate_text') }}">Text</a>
|
||||
<a href="{{ url_for('generate_image') }}">Image</a>
|
||||
<a href="{{ url_for('generate_video') }}">Video</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('profile') }}">Profile</a>
|
||||
{% if session.get('user_role') == 'admin' %}
|
||||
<a href="{{ url_for('admin') }}">Admin</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout') }}">Log out</a>
|
||||
{% else %}
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}">Log in</a>
|
||||
<a href="{{ url_for('register') }}">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div id="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<span class="spinner-label">Working…</span>
|
||||
</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %} {% for
|
||||
category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %} {% endwith %} {% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Generate — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
{% extends "base.html" %} {% block title %}Generate — AI Allucanget{% endblock
|
||||
%} {% block content %}
|
||||
<div class="card">
|
||||
<h1>Generate</h1>
|
||||
<form method="post">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" name="type">
|
||||
<option value="text">Text</option>
|
||||
<option value="image">Image</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
|
||||
<label for="model">Model</label>
|
||||
<input id="model" name="model" type="text" required placeholder="e.g. openai/gpt-4o">
|
||||
|
||||
<label for="prompt">Prompt</label>
|
||||
<textarea id="prompt" name="prompt" rows="4" required placeholder="Describe what you want…"></textarea>
|
||||
|
||||
<button type="submit">Generate</button>
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result %}
|
||||
<div class="result">
|
||||
{% if result.get('content') %}
|
||||
<h2>Result</h2>
|
||||
<pre>{{ result.content }}</pre>
|
||||
{% elif result.get('images') %}
|
||||
<h2>Generated image{{ 's' if result.images|length > 1 }}</h2>
|
||||
{% for img in result.images %}
|
||||
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
|
||||
{% endfor %}
|
||||
{% elif result.get('status') %}
|
||||
<h2>Video job</h2>
|
||||
<p>Status: <strong>{{ result.status }}</strong></p>
|
||||
{% if result.get('video_url') %}
|
||||
<video src="{{ result.video_url }}" controls class="generated-video"></video>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-muted">
|
||||
Choose a generation type from the Generate menu above.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Image Generation — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>Image Generation</h1>
|
||||
<form method="post">
|
||||
<label for="model">Model</label>
|
||||
<input id="model" name="model" type="text" required
|
||||
placeholder="e.g. openai/dall-e-3"
|
||||
value="{{ request.form.get('model', '') }}">
|
||||
|
||||
<label for="prompt">Prompt</label>
|
||||
<textarea id="prompt" name="prompt" rows="4" required
|
||||
placeholder="Describe the image you want…">{{ request.form.get('prompt', '') }}</textarea>
|
||||
|
||||
<label for="size">Size</label>
|
||||
<select id="size" name="size">
|
||||
<option value="1024x1024" {% if request.form.get('size','1024x1024')=='1024x1024' %}selected{% endif %}>1024×1024</option>
|
||||
<option value="1792x1024" {% if request.form.get('size')=='1792x1024' %}selected{% endif %}>1792×1024 (landscape)</option>
|
||||
<option value="1024x1792" {% if request.form.get('size')=='1024x1792' %}selected{% endif %}>1024×1792 (portrait)</option>
|
||||
<option value="512x512" {% if request.form.get('size')=='512x512' %}selected{% endif %}>512×512</option>
|
||||
</select>
|
||||
|
||||
<label for="n">Number of images</label>
|
||||
<select id="n" name="n">
|
||||
<option value="1" {% if request.form.get('n','1')=='1' %}selected{% endif %}>1</option>
|
||||
<option value="2" {% if request.form.get('n')=='2' %}selected{% endif %}>2</option>
|
||||
<option value="4" {% if request.form.get('n')=='4' %}selected{% endif %}>4</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Generate image</button>
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error mt-2">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result %}
|
||||
<div class="result">
|
||||
<h2>Generated image{{ 's' if result.images|length > 1 }}</h2>
|
||||
{% for img in result.images %}
|
||||
<img src="{{ img.url }}" alt="Generated image" class="generated-image">
|
||||
{% if img.revised_prompt %}
|
||||
<p class="text-muted mt-1" style="font-size:0.8rem;">{{ img.revised_prompt }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Text Generation — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>Text Generation</h1>
|
||||
<form method="post">
|
||||
<label for="model">Model</label>
|
||||
<input id="model" name="model" type="text" required
|
||||
placeholder="e.g. openai/gpt-4o"
|
||||
value="{{ request.form.get('model', '') }}">
|
||||
|
||||
<label for="prompt">Prompt</label>
|
||||
<textarea id="prompt" name="prompt" rows="5" required
|
||||
placeholder="Describe what you want…">{{ request.form.get('prompt', '') }}</textarea>
|
||||
|
||||
<button type="submit">Generate text</button>
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error mt-2">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result %}
|
||||
<div class="result">
|
||||
<h2>Result</h2>
|
||||
<pre>{{ result.content }}</pre>
|
||||
{% if result.usage %}
|
||||
<p class="text-muted mt-1" style="font-size:0.8rem;">
|
||||
Tokens: {{ result.usage.get('total_tokens', '—') }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Video Generation — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>Video Generation</h1>
|
||||
|
||||
<div class="tabs-container">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="text-to-video" type="button">Text to video</button>
|
||||
<button class="tab-btn" data-tab="image-to-video" type="button">Image to video</button>
|
||||
</div>
|
||||
|
||||
<!-- Text-to-video -->
|
||||
<div class="tab-panel active" id="tab-text-to-video">
|
||||
<form method="post">
|
||||
<input type="hidden" name="mode" value="text">
|
||||
|
||||
<label for="model-t">Model</label>
|
||||
<input id="model-t" name="model" type="text" required
|
||||
placeholder="e.g. openai/sora-2-pro"
|
||||
value="{{ request.form.get('model', '') if request.form.get('mode','text')=='text' else '' }}">
|
||||
|
||||
<label for="prompt-t">Prompt</label>
|
||||
<textarea id="prompt-t" name="prompt" rows="4" required
|
||||
placeholder="Describe the video you want…">{{ request.form.get('prompt', '') if request.form.get('mode','text')=='text' else '' }}</textarea>
|
||||
|
||||
<label for="aspect-t">Aspect ratio</label>
|
||||
<select id="aspect-t" name="aspect_ratio">
|
||||
<option value="16:9">16:9 (landscape)</option>
|
||||
<option value="9:16">9:16 (portrait)</option>
|
||||
<option value="1:1">1:1 (square)</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Generate video</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Image-to-video -->
|
||||
<div class="tab-panel" id="tab-image-to-video">
|
||||
<form method="post">
|
||||
<input type="hidden" name="mode" value="image">
|
||||
|
||||
<label for="model-i">Model</label>
|
||||
<input id="model-i" name="model" type="text" required
|
||||
placeholder="e.g. openai/sora-2-pro"
|
||||
value="{{ request.form.get('model', '') if request.form.get('mode')=='image' else '' }}">
|
||||
|
||||
<label for="image_url">Source image URL</label>
|
||||
<input id="image_url" name="image_url" type="url" required
|
||||
placeholder="https://example.com/photo.jpg"
|
||||
value="{{ request.form.get('image_url', '') }}">
|
||||
|
||||
<label for="prompt-i">Motion prompt</label>
|
||||
<textarea id="prompt-i" name="prompt" rows="3" required
|
||||
placeholder="Describe the motion or transformation…">{{ request.form.get('prompt', '') if request.form.get('mode')=='image' else '' }}</textarea>
|
||||
|
||||
<label for="aspect-i">Aspect ratio</label>
|
||||
<select id="aspect-i" name="aspect_ratio">
|
||||
<option value="16:9">16:9 (landscape)</option>
|
||||
<option value="9:16">9:16 (portrait)</option>
|
||||
<option value="1:1">1:1 (square)</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Generate video from image</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-error mt-2">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result %}
|
||||
<div class="result">
|
||||
<h2>Video job</h2>
|
||||
<p>Status: <strong>{{ result.status }}</strong></p>
|
||||
{% if result.get('video_url') %}
|
||||
<video src="{{ result.video_url }}" controls class="generated-video"></video>
|
||||
{% else %}
|
||||
<p class="text-muted mt-1" style="font-size:0.875rem;">
|
||||
Video is being processed. Check back later.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %} {% block title %}Profile — AI Allucanget{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>Your Profile</h1>
|
||||
|
||||
<h2 class="section-title" style="margin-top: 0">Account details</h2>
|
||||
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1.5rem">
|
||||
Current email:
|
||||
<strong style="color: var(--text)">{{ user.get('email', '') }}</strong>
|
||||
· Role:
|
||||
<span class="role-badge role-{{ user.get('role','user') }}"
|
||||
>{{ user.get('role', 'user') }}</span
|
||||
>
|
||||
</p>
|
||||
|
||||
<h2 class="section-title">Update email</h2>
|
||||
<form method="post">
|
||||
<label for="email">New email</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="{{ user.get('email', '') }}"
|
||||
/>
|
||||
<input type="hidden" name="password" value="" />
|
||||
<button type="submit">Save email</button>
|
||||
</form>
|
||||
|
||||
<h2 class="section-title" style="margin-top: 2rem">Change password</h2>
|
||||
<form method="post">
|
||||
<label for="password">New password</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
minlength="8"
|
||||
/>
|
||||
<input type="hidden" name="email" value="" />
|
||||
<button type="submit">Save password</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user