File size: 11,379 Bytes
16e1aa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
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