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:
+137
-24
@@ -42,6 +42,18 @@ def login_required(view):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(view):
|
||||||
|
@functools.wraps(view)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
if "access_token" not in session:
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
if session.get("user_role") != "admin":
|
||||||
|
flash("Admin access required.", "error")
|
||||||
|
return redirect(url_for("dashboard"))
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Auth routes
|
# Auth routes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -63,6 +75,11 @@ def login():
|
|||||||
data = resp.json()
|
data = resp.json()
|
||||||
session["access_token"] = data["access_token"]
|
session["access_token"] = data["access_token"]
|
||||||
session["refresh_token"] = data["refresh_token"]
|
session["refresh_token"] = data["refresh_token"]
|
||||||
|
me = _api("GET", "/users/me", token=data["access_token"])
|
||||||
|
if me.status_code == 200:
|
||||||
|
u = me.json()
|
||||||
|
session["user_email"] = u.get("email", "")
|
||||||
|
session["user_role"] = u.get("role", "user")
|
||||||
return redirect(url_for("dashboard"))
|
return redirect(url_for("dashboard"))
|
||||||
flash("Invalid email or password.", "error")
|
flash("Invalid email or password.", "error")
|
||||||
return render_template("login.html")
|
return render_template("login.html")
|
||||||
@@ -104,33 +121,129 @@ def dashboard():
|
|||||||
return render_template("dashboard.html", user=user)
|
return render_template("dashboard.html", user=user)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/generate", methods=["GET", "POST"])
|
# ── Generate ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/generate")
|
||||||
@login_required
|
@login_required
|
||||||
def generate():
|
def generate():
|
||||||
result = None
|
return redirect(url_for("generate_text"))
|
||||||
error = None
|
|
||||||
|
|
||||||
|
@app.route("/generate/text", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def generate_text():
|
||||||
|
result = error = None
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
gen_type = request.form.get("type", "text")
|
resp = _api("POST", "/generate/text", token=session["access_token"], json={
|
||||||
model = request.form.get("model", "").strip()
|
"model": request.form.get("model", "").strip(),
|
||||||
prompt = request.form.get("prompt", "").strip()
|
"prompt": request.form.get("prompt", "").strip(),
|
||||||
token = session["access_token"]
|
})
|
||||||
|
if resp.status_code == 200:
|
||||||
if gen_type == "text":
|
|
||||||
resp = _api("POST", "/generate/text", token=token,
|
|
||||||
json={"model": model, "prompt": prompt})
|
|
||||||
elif gen_type == "image":
|
|
||||||
resp = _api("POST", "/generate/image", token=token,
|
|
||||||
json={"model": model, "prompt": prompt})
|
|
||||||
elif gen_type == "video":
|
|
||||||
resp = _api("POST", "/generate/video", token=token,
|
|
||||||
json={"model": model, "prompt": prompt})
|
|
||||||
else:
|
|
||||||
resp = None
|
|
||||||
|
|
||||||
if resp is not None and resp.status_code == 200:
|
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
else:
|
else:
|
||||||
detail = resp.json().get("detail", "Generation failed.") if resp is not None else "Unknown error."
|
error = resp.json().get("detail", "Generation failed.")
|
||||||
error = detail
|
return render_template("generate_text.html", result=result, error=error)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/generate/image", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def generate_image():
|
||||||
|
result = error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
resp = _api("POST", "/generate/image", token=session["access_token"], json={
|
||||||
|
"model": request.form.get("model", "").strip(),
|
||||||
|
"prompt": request.form.get("prompt", "").strip(),
|
||||||
|
"n": int(request.form.get("n", 1)),
|
||||||
|
"size": request.form.get("size", "1024x1024"),
|
||||||
|
})
|
||||||
|
if resp.status_code == 200:
|
||||||
|
result = resp.json()
|
||||||
|
else:
|
||||||
|
error = resp.json().get("detail", "Generation failed.")
|
||||||
|
return render_template("generate_image.html", result=result, error=error)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/generate/video", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def generate_video():
|
||||||
|
result = error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
mode = request.form.get("mode", "text")
|
||||||
|
token = session["access_token"]
|
||||||
|
if mode == "image":
|
||||||
|
resp = _api("POST", "/generate/video/from-image", token=token, json={
|
||||||
|
"model": request.form.get("model", "").strip(),
|
||||||
|
"image_url": request.form.get("image_url", "").strip(),
|
||||||
|
"prompt": request.form.get("prompt", "").strip(),
|
||||||
|
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
resp = _api("POST", "/generate/video", token=token, json={
|
||||||
|
"model": request.form.get("model", "").strip(),
|
||||||
|
"prompt": request.form.get("prompt", "").strip(),
|
||||||
|
"aspect_ratio": request.form.get("aspect_ratio", "16:9"),
|
||||||
|
})
|
||||||
|
if resp.status_code == 200:
|
||||||
|
result = resp.json()
|
||||||
|
else:
|
||||||
|
error = resp.json().get("detail", "Generation failed.")
|
||||||
|
return render_template("generate_video.html", result=result, error=error)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Admin ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.get("/admin")
|
||||||
|
@admin_required
|
||||||
|
def admin():
|
||||||
|
token = session["access_token"]
|
||||||
|
stats_resp = _api("GET", "/admin/stats", token=token)
|
||||||
|
users_resp = _api("GET", "/users", token=token)
|
||||||
|
stats = stats_resp.json() if stats_resp.status_code == 200 else {}
|
||||||
|
users = users_resp.json() if users_resp.status_code == 200 else []
|
||||||
|
return render_template("admin.html", stats=stats, users=users)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/users/<user_id>/role")
|
||||||
|
@admin_required
|
||||||
|
def admin_set_role(user_id: str):
|
||||||
|
role = request.form.get("role", "user")
|
||||||
|
_api("PUT", f"/users/{user_id}/role", token=session["access_token"], json={"role": role})
|
||||||
|
flash(f"Role updated to '{role}'.", "success")
|
||||||
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/admin/users/<user_id>/delete")
|
||||||
|
@admin_required
|
||||||
|
def admin_delete_user(user_id: str):
|
||||||
|
_api("DELETE", f"/users/{user_id}", token=session["access_token"])
|
||||||
|
flash("User deleted.", "success")
|
||||||
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Profile ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.route("/users/profile", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
token = session["access_token"]
|
||||||
|
if request.method == "POST":
|
||||||
|
payload: dict = {}
|
||||||
|
new_email = request.form.get("email", "").strip()
|
||||||
|
new_password = request.form.get("password", "").strip()
|
||||||
|
if new_email:
|
||||||
|
payload["email"] = new_email
|
||||||
|
if new_password:
|
||||||
|
payload["password"] = new_password
|
||||||
|
if payload:
|
||||||
|
resp = _api("PUT", "/users/me", token=token, json=payload)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
updated = resp.json()
|
||||||
|
session["user_email"] = updated.get("email", session.get("user_email", ""))
|
||||||
|
flash("Profile updated.", "success")
|
||||||
|
else:
|
||||||
|
flash(resp.json().get("detail", "Update failed."), "error")
|
||||||
|
return redirect(url_for("profile"))
|
||||||
|
resp = _api("GET", "/users/me", token=token)
|
||||||
|
user = resp.json() if resp.status_code == 200 else {}
|
||||||
|
return render_template("profile.html", user=user)
|
||||||
|
|
||||||
return render_template("generate.html", result=result, error=error)
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// ── Loading overlay ────────────────────────────────────
|
||||||
|
const overlay = document.getElementById('loading-overlay');
|
||||||
|
|
||||||
|
document.querySelectorAll('form').forEach((form) => {
|
||||||
|
form.addEventListener('submit', () => {
|
||||||
|
if (overlay) overlay.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Hamburger menu ─────────────────────────────────────
|
||||||
|
const hamburger = document.querySelector('.hamburger');
|
||||||
|
const navLinks = document.querySelector('.nav-links');
|
||||||
|
|
||||||
|
if (hamburger && navLinks) {
|
||||||
|
hamburger.addEventListener('click', () => {
|
||||||
|
navLinks.classList.toggle('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generate dropdown tabs ─────────────────────────────
|
||||||
|
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const target = btn.dataset.tab;
|
||||||
|
const container = btn.closest('.tabs-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
|
||||||
|
container.querySelectorAll('.tab-panel').forEach((p) => p.classList.remove('active'));
|
||||||
|
|
||||||
|
btn.classList.add('active');
|
||||||
|
const panel = container.querySelector(`#tab-${target}`);
|
||||||
|
if (panel) panel.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,21 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #0f1117;
|
||||||
|
--surface: #1a1d27;
|
||||||
|
--surface-2: #22263a;
|
||||||
|
--border: #2e3250;
|
||||||
|
--text: #e8eaf6;
|
||||||
|
--text-muted: #8b90b8;
|
||||||
|
--accent: #7c6ff7;
|
||||||
|
--accent-hover: #9d97ff;
|
||||||
|
--danger: #e05a6a;
|
||||||
|
--danger-hover: #f07080;
|
||||||
|
--success: #56c489;
|
||||||
|
--warning: #f0b429;
|
||||||
|
--radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
@@ -8,19 +26,525 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
|
"Inter",
|
||||||
system-ui,
|
system-ui,
|
||||||
-apple-system,
|
-apple-system,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
background: #f5f5f5;
|
background: var(--bg);
|
||||||
color: #222;
|
color: var(--text);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nav */
|
/* ─── Nav ──────────────────────────────────────────────── */
|
||||||
header {
|
header {
|
||||||
background: #1a1a2e;
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 3.5rem;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
color: var(--accent-hover);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition:
|
||||||
|
color 0.15s,
|
||||||
|
background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown */
|
||||||
|
.nav-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
left: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown:hover .nav-dropdown-menu,
|
||||||
|
.nav-dropdown.open .nav-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-menu a {
|
||||||
|
display: block;
|
||||||
|
padding: 0.55rem 1rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger */
|
||||||
|
.hamburger {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger span {
|
||||||
|
display: block;
|
||||||
|
width: 22px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--text-muted);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Main layout ──────────────────────────────────────── */
|
||||||
|
main {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Alerts ───────────────────────────────────────────── */
|
||||||
|
.alert {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: rgba(86, 196, 137, 0.15);
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid rgba(86, 196, 137, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background: rgba(224, 90, 106, 0.15);
|
||||||
|
color: var(--danger);
|
||||||
|
border: 1px solid rgba(224, 90, 106, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Card ─────────────────────────────────────────────── */
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Forms ────────────────────────────────────────────── */
|
||||||
|
form label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input,
|
||||||
|
form select,
|
||||||
|
form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.6rem 0.85rem;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--text);
|
||||||
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input::placeholder,
|
||||||
|
form textarea::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
form select option {
|
||||||
|
background: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:focus,
|
||||||
|
form select:focus,
|
||||||
|
form textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(124, 111, 247, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Buttons ──────────────────────────────────────────── */
|
||||||
|
.btn,
|
||||||
|
button[type="submit"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover,
|
||||||
|
button[type="submit"]:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active,
|
||||||
|
button[type="submit"]:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Tabs ─────────────────────────────────────────────── */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 0;
|
||||||
|
transition:
|
||||||
|
color 0.15s,
|
||||||
|
border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--accent-hover);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Result ───────────────────────────────────────────── */
|
||||||
|
.result {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-image {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-video {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Admin table ──────────────────────────────────────── */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box .stat-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box .stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-hover);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-admin {
|
||||||
|
background: rgba(124, 111, 247, 0.2);
|
||||||
|
color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-user {
|
||||||
|
background: rgba(139, 144, 184, 0.15);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Loading overlay ──────────────────────────────────── */
|
||||||
|
#loading-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 17, 23, 0.75);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Misc ─────────────────────────────────────────────── */
|
||||||
|
.text-muted {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Responsive ───────────────────────────────────────── */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hamburger {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: absolute;
|
||||||
|
top: 3.5rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
gap: 0.1rem;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links.open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-dropdown-menu {
|
||||||
|
position: static;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -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,19 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}AI Allucanget{% endblock %}</title>
|
<title>{% block title %}AI Allucanget{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ url_for('static', filename='style.css') }}"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="{{ url_for('index') }}" class="brand">AI Allucanget</a>
|
<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">
|
<div class="nav-links">
|
||||||
{% if session.get('access_token') %}
|
{% if session.get('access_token') %}
|
||||||
<a href="{{ url_for('dashboard') }}">Dashboard</a>
|
<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>
|
<a href="{{ url_for('logout') }}">Log out</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('login') }}">Log in</a>
|
<a href="{{ url_for('login') }}">Log in</a>
|
||||||
@@ -23,14 +44,18 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<div id="loading-overlay">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
<div class="spinner"></div>
|
||||||
{% for category, message in messages %}
|
<span class="spinner-label">Working…</span>
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
<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>
|
</main>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,47 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}Generate — AI Allucanget{% endblock
|
||||||
{% block title %}Generate — AI Allucanget{% endblock %}
|
%} {% block content %}
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>Generate</h1>
|
<h1>Generate</h1>
|
||||||
<form method="post">
|
<p class="text-muted">
|
||||||
<label for="type">Type</label>
|
Choose a generation type from the Generate menu above.
|
||||||
<select id="type" name="type">
|
</p>
|
||||||
<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 %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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