simikkk's picture
Upload 36 files
16e1aa7 verified
Raw
History Blame Contribute Delete
11.4 kB
"""
Gradio Blocks UI, mounted onto the FastAPI app in main.py.
Design choice: the UI talks to the *same* FastAPI routes (auth, generate,
brand-voice, history, billing) via an in-process ASGI HTTP client, rather
than calling the underlying Python functions directly. This means every rule
enforced in the routes (rate limits, tier gating, quota checks, validation)
applies identically whether a request comes from the UI or from a future
external API consumer - no duplicated / drifting business logic.
"""
import httpx
import gradio as gr
from app.config import DEMO_MODE, TIERS, Tier
def _client(app) -> httpx.AsyncClient:
transport = httpx.ASGITransport(app=app)
return httpx.AsyncClient(transport=transport, base_url="http://internal")
def build_ui(app) -> gr.Blocks:
with gr.Blocks(title="Etsy Listing Optimizer") as demo:
token_state = gr.State(value=None)
email_state = gr.State(value=None)
gr.Markdown("# 🏷️ Etsy Listing Optimizer")
if DEMO_MODE:
gr.Markdown(
"⚠️ **Demo mode**: no real Supabase/Groq/Gemini/Stripe credentials are configured, "
"so this is running on in-memory storage and a mock AI provider that returns "
"already-valid sample output. Add real API keys (see `.env.example` / `DEPLOY.md`) "
"to go live. Signup/login works with a local in-memory demo auth store — data is "
"lost on restart."
)
with gr.Tab("Account"):
gr.Markdown("### Sign up or log in")
with gr.Row():
email_box = gr.Textbox(label="Email")
password_box = gr.Textbox(label="Password", type="password")
with gr.Row():
signup_btn = gr.Button("Sign up")
login_btn = gr.Button("Log in")
logout_btn = gr.Button("Log out")
auth_status = gr.Markdown("Not logged in.")
async def do_signup(email, password):
async with _client(app) as client:
resp = await client.post("/auth/signup", json={"email": email, "password": password})
if resp.status_code != 200:
return None, None, f"❌ {resp.json().get('detail', 'Sign up failed.')}"
data = resp.json()
return data["access_token"], data["email"], f"✅ Signed up and logged in as {data['email']}."
async def do_login(email, password):
async with _client(app) as client:
resp = await client.post("/auth/login", json={"email": email, "password": password})
if resp.status_code != 200:
return None, None, f"❌ {resp.json().get('detail', 'Login failed.')}"
data = resp.json()
return data["access_token"], data["email"], f"✅ Logged in as {data['email']}."
def do_logout():
return None, None, "Logged out."
signup_btn.click(do_signup, [email_box, password_box], [token_state, email_state, auth_status])
login_btn.click(do_login, [email_box, password_box], [token_state, email_state, auth_status])
logout_btn.click(do_logout, None, [token_state, email_state, auth_status])
gr.Markdown("### Plans")
plan_lines = []
for tier in [Tier.FREE, Tier.STARTER, Tier.PRO, Tier.BUSINESS]:
c = TIERS[tier]
variants = f"{c.title_variants} title variant(s)"
gens = "unlimited generations" if c.generations_per_month == -1 else f"{c.generations_per_month} generations/mo"
plan_lines.append(f"- **{c.display_name}** ({c.price_czk} CZK/mo): {gens}, {variants}")
gr.Markdown("\n".join(plan_lines))
with gr.Row():
upgrade_tier = gr.Dropdown(
choices=[Tier.STARTER.value, Tier.PRO.value, Tier.BUSINESS.value],
label="Upgrade to",
value=Tier.STARTER.value,
)
upgrade_btn = gr.Button("Get checkout link")
checkout_output = gr.Markdown()
async def do_checkout(tier, token):
if not token:
return "Log in first."
async with _client(app) as client:
resp = await client.post(
f"/billing/checkout/{tier}", headers={"Authorization": f"Bearer {token}"}
)
if resp.status_code != 200:
return f"❌ {resp.json().get('detail', 'Checkout unavailable.')}"
return f"[Click here to complete checkout]({resp.json()['checkout_url']})"
upgrade_btn.click(do_checkout, [upgrade_tier, token_state], checkout_output)
gr.Markdown(
"### Your data\n"
"Use the buttons below for GDPR data export / account deletion."
)
with gr.Row():
export_btn = gr.Button("Export my data (JSON)")
delete_btn = gr.Button("🗑️ Delete my account", variant="stop")
account_status = gr.Markdown()
async def do_export(token):
if not token:
return "Log in first."
async with _client(app) as client:
resp = await client.get("/account/export", headers={"Authorization": f"Bearer {token}"})
return f"```json\n{resp.text}\n```"
async def do_delete(token):
if not token:
return None, None, "Log in first."
async with _client(app) as client:
resp = await client.delete("/account/delete", headers={"Authorization": f"Bearer {token}"})
if resp.status_code != 200:
return token, None, f"❌ {resp.json().get('detail', 'Delete failed.')}"
return None, None, "✅ Account and all data deleted."
export_btn.click(do_export, token_state, account_status)
delete_btn.click(do_delete, token_state, [token_state, email_state, account_status])
with gr.Tab("Generate listing"):
gr.Markdown(
"Describe your product in plain language. We'll generate SEO-optimized "
"title(s), exactly 13 tags, and a description — then validate everything "
"programmatically (length limits, tag count, no duplicate words) before "
"showing it to you."
)
product_description = gr.Textbox(
label="Product description",
placeholder="Hand-poured soy candle in a reused wine bottle, lavender and vanilla scent...",
lines=4,
)
target_keywords = gr.Textbox(label="Target keywords (optional)", placeholder="e.g. eco friendly gift")
generate_btn = gr.Button("Generate", variant="primary")
quota_md = gr.Markdown()
titles_out = gr.Textbox(label="Title variant(s)", lines=3, interactive=False)
tags_out = gr.Textbox(label="Tags (13)", lines=3, interactive=False)
description_out = gr.Textbox(label="Description", lines=6, interactive=False)
category_out = gr.Textbox(label="Category/attribute hints (Pro/Business)", interactive=False)
async def do_generate(description, keywords, token):
if not token:
return "Log in first (Account tab).", "", "", "", ""
async with _client(app) as client:
resp = await client.post(
"/generate",
json={"product_description": description, "target_keywords": keywords or None},
headers={"Authorization": f"Bearer {token}"},
)
if resp.status_code != 200:
detail = resp.json().get("detail", "Generation failed.")
return f"❌ {detail}", "", "", "", ""
data = resp.json()
quota = (
f"Used {data['generations_used_this_month']} / "
f"{data['generations_limit_this_month']} generations this month."
)
return (
quota,
"\n".join(f"{i+1}. {t}" for i, t in enumerate(data["titles"])),
", ".join(data["tags"]),
data["description"],
", ".join(data["category_hints"]) if data["category_hints"] else "(none for your plan)",
)
generate_btn.click(
do_generate,
[product_description, target_keywords, token_state],
[quota_md, titles_out, tags_out, description_out, category_out],
)
with gr.Tab("Brand voice"):
gr.Markdown("Set a brand voice profile to automatically flavor every generation.")
bv_name = gr.Textbox(label="Profile name", value="Default")
bv_tone = gr.Textbox(label="Tone (e.g. playful, elegant, minimalist, or free text)")
bv_audience = gr.Textbox(label="Target audience")
bv_words = gr.Textbox(label="Favorite words/phrases to reuse")
bv_save_btn = gr.Button("Save profile")
bv_status = gr.Markdown()
bv_list = gr.JSON(label="Your brand voice profiles")
async def save_brand_voice(name, tone, audience, words, token):
if not token:
return "Log in first.", []
async with _client(app) as client:
resp = await client.post(
"/brand-voice",
json={"name": name, "tone": tone, "target_audience": audience, "favorite_words": words},
headers={"Authorization": f"Bearer {token}"},
)
if resp.status_code != 200:
return f"❌ {resp.json().get('detail', 'Save failed.')}", []
listing = await client.get("/brand-voice", headers={"Authorization": f"Bearer {token}"})
return "✅ Saved.", listing.json()
bv_save_btn.click(
save_brand_voice, [bv_name, bv_tone, bv_audience, bv_words, token_state], [bv_status, bv_list]
)
with gr.Tab("History"):
gr.Markdown("Past generations (visibility depends on your plan).")
history_refresh_btn = gr.Button("Refresh history")
history_json = gr.JSON()
async def refresh_history(token):
if not token:
return {"error": "Log in first."}
async with _client(app) as client:
resp = await client.get("/history", headers={"Authorization": f"Bearer {token}"})
if resp.status_code != 200:
return {"error": resp.json().get("detail", "History unavailable.")}
return resp.json()
history_refresh_btn.click(refresh_history, token_state, history_json)
gr.Markdown(
"[Privacy Policy](/legal/privacy) · [Terms of Service](/legal/terms) · "
"[Refund Policy](/legal/refunds)"
)
return demo