| import os |
| import re |
| import time |
| import json |
| import bcrypt |
| import secrets |
| import hashlib |
| import requests |
| import stripe |
| import logging |
| import tempfile |
| from datetime import datetime, timedelta |
| from io import BytesIO |
|
|
| import gradio as gr |
| import pandas as pd |
| from PIL import Image |
| import pytesseract |
| from pdf2image import convert_from_bytes |
| from supabase import create_client, Client |
| from groq import Groq |
|
|
| |
| |
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| SUPABASE_URL = os.getenv("SUPABASE_URL") |
| SUPABASE_KEY = os.getenv("SUPABASE_KEY") |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") |
| STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") |
| APP_URL = os.getenv("APP_URL", "http://localhost:7860") |
| STRIPE_PRICE_BASIC = os.getenv("STRIPE_PRICE_BASIC", "price_basic") |
| STRIPE_PRICE_PRO = os.getenv("STRIPE_PRICE_PRO", "price_pro") |
| STRIPE_PRICE_ENTERPRISE = os.getenv("STRIPE_PRICE_ENTERPRISE", "price_ent") |
|
|
| stripe.api_key = STRIPE_SECRET_KEY |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) if SUPABASE_URL and SUPABASE_KEY else None |
| groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None |
|
|
| class RateLimiter: |
| def __init__(self): self.requests = {} |
| def is_rate_limited(self, key: str, limit: int, period: int) -> bool: |
| now = time.time() |
| if key not in self.requests: self.requests[key] = [] |
| self.requests[key] = [t for t in self.requests[key] if now - t < period] |
| if len(self.requests[key]) >= limit: return True |
| self.requests[key].append(now) |
| return False |
|
|
| rate_limiter = RateLimiter() |
|
|
| def check_password_leaked(password: str) -> bool: |
| sha1 = hashlib.sha1(password.encode('utf-8')).hexdigest().upper() |
| prefix, suffix = sha1[:5], sha1[5:] |
| try: |
| r = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}', timeout=3) |
| if suffix in r.text: return True |
| except: pass |
| return False |
|
|
| def validate_password_strength(password: str) -> bool: |
| if len(password) < 12: return False |
| if not re.search(r"[A-Z]", password): return False |
| if not re.search(r"\d", password): return False |
| if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password): return False |
| return True |
|
|
| def hash_password(password: str) -> str: |
| return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12)).decode('utf-8') |
|
|
| def verify_password(password: str, hashed: str) -> bool: |
| return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8')) |
|
|
| |
| |
| |
| def ocr_tesseract(image: Image.Image) -> str: |
| try: return pytesseract.image_to_string(image, lang='eng') |
| except: return "" |
|
|
| def extract_with_groq(text: str) -> dict: |
| if not groq_client: return {} |
| prompt = f"Extract invoice data. Return ONLY JSON. Keys: vendor, inv_number, inv_date, due_date, amount, vat_amount, total, currency. Text: {text[:3000]}" |
| try: |
| chat_completion = groq_client.chat.completions.create( |
| messages=[{"role": "user", "content": prompt}], model="llama-3.1-8b-instant", max_tokens=512, temperature=0.05 |
| ) |
| return json.loads(chat_completion.choices[0].message.content.strip().strip('```json')) |
| except: return {} |
|
|
| def process_invoice_file(file_path: str, user_id: int) -> dict: |
| try: |
| with open(file_path, 'rb') as f: file_bytes = f.read() |
| images = [] |
| if file_path.lower().endswith('.pdf'): images = convert_from_bytes(file_bytes) |
| else: images = [Image.open(BytesIO(file_bytes))] |
| |
| full_text = "" |
| for img in images: full_text += ocr_tesseract(img) + "\n" |
| |
| data = extract_with_groq(full_text) |
| if not data: |
| inv_match = re.search(r'(?:invoice|inv)[#:\s]+([A-Z0-9-]{4,24})', full_text, re.I) |
| if inv_match: data['inv_number'] = inv_match.group(1) |
| total_match = re.search(r'(?:total|amount due)[\s:$]+([0-9,\.]+)', full_text, re.I) |
| if total_match: data['total'] = float(total_match.group(1).replace(',', '')) |
| lines = [l.strip() for l in full_text.split('\n') if l.strip()] |
| if lines: data['vendor'] = lines[0] |
|
|
| data['filename'] = os.path.basename(file_path) |
| data['status'] = 'done' |
| if supabase: |
| supabase.table("invoices").insert({"user_id": user_id, "filename": data.get('filename'), "vendor": data.get('vendor'), "inv_number": data.get('inv_number'), "total": data.get('total'), "status": "done", "confidence": 0.95}).execute() |
| return data |
| except Exception as e: return {"filename": os.path.basename(file_path), "status": "error"} |
|
|
| |
| |
| |
| CUSTOM_CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); |
| :root { |
| --bg: #09090b; |
| --panel: #0f0f12; |
| --border: #1f1f23; |
| --text: #f4f4f5; |
| --muted: #71717a; |
| --accent: #6366f1; /* Indigo */ |
| --accent-hover: #4f46e5; |
| --accent-glow: rgba(99, 102, 241, 0.2); |
| --success: #10b981; |
| } |
| * { box-sizing: border-box; font-family: 'Inter', sans-serif; } |
| body { background: var(--bg); color: var(--text); margin: 0; background-image: radial-gradient(circle at 50% 0%, rgba(99, 102, 241, 0.08) 0%, transparent 40%); } |
| .gradio-container { max-width: 100% !important; padding: 0 !important; background: transparent !important; min-height: 100vh; } |
| .hide { display: none !important; } |
| |
| /* Navbar */ |
| .navbar { display: flex; justify-content: space-between; align-items: center; padding: 20px 60px; border-bottom: 1px solid var(--border); backdrop-filter: blur(10px); position: sticky; top: 0; background: rgba(9,9,11,0.8); z-index: 100; } |
| .logo { font-weight: 700; font-size: 20px; display: flex; align-items: center; gap: 8px; color: var(--text); } |
| .logo svg { width: 24px; height: 24px; fill: var(--accent); } |
| .nav-links { display: flex; gap: 32px; align-items: center; } |
| .nav-link { color: var(--muted); font-size: 14px; font-weight: 500; text-decoration: none; transition: 0.2s; } |
| .nav-link:hover { color: var(--text); } |
| |
| /* Layout */ |
| .main-container { max-width: 1200px; margin: 0 auto; padding: 60px 40px; } |
| .hero { text-align: center; padding: 80px 20px; } |
| .hero h1 { font-size: 64px; font-weight: 800; margin-bottom: 24px; background: linear-gradient(180deg, #fff 0%, #a1a1aa 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.1; letter-spacing: -2px; } |
| .hero p { font-size: 20px; color: var(--muted); max-width: 600px; margin: 0 auto 40px; } |
| .badges { display: flex; justify-content: center; gap: 20px; margin-top: 40px; color: var(--muted); font-size: 13px; font-weight: 500; } |
| .badge { display: flex; align-items: center; gap: 6px; border: 1px solid var(--border); padding: 6px 12px; border-radius: 20px; background: var(--panel); } |
| |
| /* Dashboard */ |
| .dash-grid { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 20px; margin-bottom: 40px; } |
| .stat-card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 24px; } |
| .stat-label { color: var(--muted); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; } |
| .stat-value { font-size: 28px; font-weight: 700; color: var(--text); } |
| |
| .dropzone { background: var(--panel) !important; border: 2px dashed var(--border) !important; border-radius: 16px !important; padding: 60px !important; text-align: center !important; transition: 0.2s !important; } |
| .dropzone:hover { border-color: var(--accent) !important; background: var(--accent-glow) !important; } |
| |
| /* Gradio Overrides */ |
| .gr-button { |
| background: var(--panel) !important; |
| border: 1px solid var(--border) !important; |
| color: var(--text) !important; |
| border-radius: 8px !important; |
| padding: 10px 20px !important; |
| font-weight: 500 !important; |
| transition: 0.2s !important; |
| box-shadow: none !important; |
| font-size: 14px !important; |
| } |
| .gr-button:hover { border-color: var(--text) !important; } |
| .btn-primary { background: var(--accent) !important; border: none !important; color: #fff !important; font-weight: 600 !important; } |
| .btn-primary:hover { background: var(--accent-hover) !important; box-shadow: 0 0 20px var(--accent-glow) !important; } |
| .btn-ghost { background: transparent !important; border: 1px solid var(--border) !important; } |
| |
| .gr-textbox { background: var(--panel) !important; border: 1px solid var(--border) !important; border-radius: 8px !important; color: var(--text) !important; } |
| .gr-textbox input { color: var(--text) !important; } |
| .gr-textbox:focus { border-color: var(--accent) !important; } |
| .gr-form { background: transparent !important; border: none !important; } |
| .gr-box { border: none !important; background: transparent !important; } |
| |
| .gr-dataframe { border: 1px solid var(--border) !important; border-radius: 12px !important; overflow: hidden; background: var(--panel) !important; } |
| .gr-dataframe table { background: transparent !important; color: var(--text) !important; border-collapse: collapse; width: 100%; } |
| .gr-dataframe th { background: rgba(0,0,0,0.3) !important; border-bottom: 1px solid var(--border) !important; color: var(--muted) !important; text-align: left; padding: 12px !important; font-size: 12px !important; text-transform: uppercase; letter-spacing: 1px; } |
| .gr-dataframe td { border-bottom: 1px solid var(--border) !important; padding: 12px !important; } |
| |
| .gr-markdown { color: var(--text) !important; background: transparent !important; } |
| footer { display: none !important; } |
| """ |
|
|
| |
| |
| |
| def get_dashboard_stats(user_dict): |
| if not user_dict or not supabase: |
| return 0, "$0.00", 0, "--" |
| res = supabase.table("invoices").select("total, is_duplicate, confidence").eq("user_id", user_dict['id']).execute() |
| data = res.data |
| total_count = len(data) |
| total_amount = sum([d['total'] for d in data if d.get('total')]) |
| duplicates = sum([1 for d in data if d.get('is_duplicate')]) |
| avg_conf = sum([d['confidence'] for d in data if d.get('confidence')]) / total_count if total_count > 0 else 0 |
| |
| return total_count, f"${total_amount:,.2f}", duplicates, f"{avg_conf*100:.1f}%" if total_count > 0 else "--" |
|
|
| def get_user_invoices(user_dict): |
| if not user_dict or not supabase: return pd.DataFrame() |
| res = supabase.table("invoices").select("*").eq("user_id", user_dict['id']).order("created_at", desc=True).limit(100).execute() |
| return pd.DataFrame(res.data) |
|
|
| def handle_auth(email, password, request: gr.Request): |
| ip = request.client.host if request else "127.0.0.1" |
| if rate_limiter.is_rate_limited(f"auth_{ip}", 5, 300): |
| return None, "🚨 Too many attempts.", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
| |
| if not supabase: return None, "DB Error", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
| |
| res = supabase.table("users").select("*").eq("email", email).execute() |
| if res.data and verify_password(password, res.data[0]['password']): |
| user = res.data[0] |
| return user, "Login successful.", gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) |
| |
| if not res.data: |
| if not validate_password_strength(password): |
| return None, "Weak password (min 12 chars, 1 upper, 1 number, 1 symbol).", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
| hashed = hash_password(password) |
| new_user = supabase.table("users").insert({"email": email, "password": hashed, "plan": "free"}).execute() |
| user = new_user.data[0] |
| return user, "Account created!", gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) |
| |
| return None, "Invalid credentials.", gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
|
|
| def handle_upload(files, user_dict): |
| if not user_dict: return "Not logged in.", get_user_invoices(user_dict) |
| if not files: return "No files.", get_user_invoices(user_dict) |
| for file in files: |
| if os.path.getsize(file.name) > 20 * 1024 * 1024: continue |
| process_invoice_file(file.name, user_dict['id']) |
| return f"Processed {len(files)} files.", get_user_invoices(user_dict) |
|
|
| def create_stripe_checkout(plan: str, user_dict): |
| if not user_dict or not STRIPE_SECRET_KEY: return "Payments not configured." |
| prices = {"basic": STRIPE_PRICE_BASIC, "pro": STRIPE_PRICE_PRO, "enterprise": STRIPE_PRICE_ENTERPRISE} |
| try: |
| session = stripe.checkout.Session.create( |
| payment_method_types=['card'], line_items=[{"price": prices[plan], "quantity": 1}], |
| mode='subscription', success_url=f"{APP_URL}?checkout=success&session_id={{CHECKOUT_SESSION_ID}}", |
| cancel_url=f"{APP_URL}?checkout=cancel", customer_email=user_dict['email'], |
| metadata={"user_id": user_dict['id'], "plan": plan} |
| ) |
| return f"[Complete Payment Here]({session.url})" |
| except Exception as e: return f"Error: {e}" |
|
|
| |
| |
| |
| with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Default(primary_hue="indigo", neutral_hue="zinc")) as app: |
| user_state = gr.State(None) |
| |
| ICON_BOLT = '<svg viewBox="0 0 24 24"><path d="M13 2L3 14h7l-1 8 10-12h-7l1-8z"/></svg>' |
| |
| |
| with gr.Group(visible=True) as landing_view: |
| with gr.Row(elem_classes="navbar"): |
| gr.HTML(f"<div class='logo'>{ICON_BOLT} OmniParse</div>") |
| with gr.Row(elem_classes="nav-links"): |
| gr.HTML("<a href='#features' class='nav-link'>Features</a>") |
| gr.HTML("<a href='#pricing' class='nav-link'>Pricing</a>") |
| landing_login_btn = gr.Button("Log In", size="sm", elem_classes="btn-ghost") |
| landing_signup_btn = gr.Button("Get Started", size="sm", elem_classes="btn-primary") |
| |
| with gr.Column(elem_classes="hero"): |
| gr.HTML("<h1>Invoice Processing, Reimagined</h1>") |
| gr.HTML("<p>AI extracts vendor, dates, amounts and line items from any PDF or image in seconds. Export to CSV, JSON or Excel.</p>") |
| with gr.Row(elem_classes="hero-cta", justify="center"): |
| cta_start = gr.Button("Start Free Trial", elem_classes="btn-primary", size="lg") |
| cta_how = gr.Button("See How It Works", elem_classes="btn-ghost", size="lg") |
| gr.HTML(""" |
| <div class="badges"> |
| <div class="badge">⚡ Powered by advanced AI</div> |
| <div class="badge">🔒 SOC 2 Compliant</div> |
| </div> |
| """) |
| |
| with gr.Column(elem_classes="main-container"): |
| gr.HTML("<h2 style='text-align:center; margin-bottom:40px;'>Everything You Need</h2>") |
| gr.HTML(""" |
| <div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:20px;"> |
| <div class="stat-card"><h3>Instant Extraction</h3><p style="color:var(--muted); font-size:14px;">Extract all invoice fields in under 3 seconds with 99% accuracy.</p></div> |
| <div class="stat-card"><h3>Duplicate Detection</h3><p style="color:var(--muted); font-size:14px;">Automatically flags duplicate invoices to prevent double payments.</p></div> |
| <div class="stat-card"><h3>AI Chat Assistant</h3><p style="color:var(--muted); font-size:14px;">Ask questions about your invoices and get instant insights.</p></div> |
| <div class="stat-card"><h3>Multi-Currency</h3><p style="color:var(--muted); font-size:14px;">Process invoices in multiple currencies with automatic conversion.</p></div> |
| <div class="stat-card"><h3>Bulk Processing</h3><p style="color:var(--muted); font-size:14px;">Upload hundreds of invoices at once and process them in parallel.</p></div> |
| <div class="stat-card"><h3>Enterprise Security</h3><p style="color:var(--muted); font-size:14px;">Your data is encrypted at rest and in transit. SOC 2 compliant.</p></div> |
| </div> |
| """) |
|
|
| |
| with gr.Group(visible=False) as auth_view: |
| with gr.Column(elem_classes="hero"): |
| gr.HTML(f"<div class='logo' style='justify-content:center; margin-bottom:24px;'>{ICON_BOLT} OmniParse</div>") |
| gr.HTML("<h1 style='font-size:48px;'>Welcome back</h1>") |
| auth_email = gr.Textbox(label="Email", placeholder="you@company.com") |
| auth_pass = gr.Textbox(label="Password", type="password", placeholder="Min 12 chars, 1 upper, 1 symbol") |
| auth_submit = gr.Button("Sign In / Sign Up", elem_classes="btn-primary", size="lg") |
| auth_msg = gr.Markdown("") |
| auth_back = gr.Button("Back to Home", elem_classes="btn-ghost", size="sm") |
|
|
| |
| with gr.Group(visible=False) as dash_view: |
| with gr.Row(elem_classes="navbar"): |
| gr.HTML(f"<div class='logo'>{ICON_BOLT} OmniParse</div>") |
| with gr.Row(elem_classes="nav-links"): |
| dash_nav_upload = gr.Button("Upload", elem_classes="btn-ghost", size="sm") |
| dash_nav_inv = gr.Button("Invoices", elem_classes="btn-ghost", size="sm") |
| dash_logout = gr.Button("Log Out", elem_classes="btn-ghost", size="sm") |
| |
| with gr.Column(elem_classes="main-container"): |
| |
| with gr.Row(elem_classes="dash-grid"): |
| stat_total = gr.HTML("<div class='stat-card'><div class='stat-label'>Total Invoices</div><div class='stat-value'>0</div></div>") |
| stat_amount = gr.HTML("<div class='stat-card'><div class='stat-label'>Total Amount</div><div class='stat-value'>$0.00</div></div>") |
| stat_dupes = gr.HTML("<div class='stat-card'><div class='stat-label'>Duplicates</div><div class='stat-value'>0</div></div>") |
| stat_conf = gr.HTML("<div class='stat-card'><div class='stat-label'>Avg Confidence</div><div class='stat-value'>--</div></div>") |
| |
| |
| with gr.Group(visible=True) as panel_upload: |
| gr.HTML("<h2 style='margin-bottom:20px;'>Welcome back! Upload your invoices</h2>") |
| file_input = gr.File(file_count="multiple", file_types=[".pdf", ".png", ".jpg"], elem_classes="dropzone") |
| process_btn = gr.Button("Process Invoices", elem_classes="btn-primary", variant="primary") |
| upload_status = gr.Markdown("") |
| |
| |
| with gr.Group(visible=False) as panel_invoices: |
| gr.HTML("<h2 style='margin-bottom:20px;'>Processed Invoices</h2>") |
| invoices_table = gr.Dataframe(headers=["Vendor", "Invoice#", "Total", "Status"], wrap=True, interactive=False) |
| with gr.Row(): |
| export_btn = gr.Button("Export to CSV", elem_classes="btn-primary") |
| up_basic = gr.Button("Upgrade to Basic", elem_classes="btn-ghost") |
| up_pro = gr.Button("Upgrade to Pro", elem_classes="btn-ghost") |
| csv_file = gr.File(label="Download CSV", visible=False) |
| checkout_link = gr.Markdown("") |
|
|
| |
| |
| |
| def show_auth(): return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) |
| def show_landing(): return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
| def show_dashboard(user): |
| if not user: return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) |
| |
| total, amount, dupes, conf = get_dashboard_stats(user) |
| stat_total_html = f"<div class='stat-card'><div class='stat-label'>Total Invoices</div><div class='stat-value'>{total}</div></div>" |
| stat_amount_html = f"<div class='stat-card'><div class='stat-label'>Total Amount</div><div class='stat-value'>{amount}</div></div>" |
| stat_dupes_html = f"<div class='stat-card'><div class='stat-label'>Duplicates</div><div class='stat-value'>{dupes}</div></div>" |
| stat_conf_html = f"<div class='stat-card'><div class='stat-label'>Avg Confidence</div><div class='stat-value'>{conf}</div></div>" |
| return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), stat_total_html, stat_amount_html, stat_dupes_html, stat_conf_html |
|
|
| landing_login_btn.click(fn=show_auth, outputs=[landing_view, auth_view, dash_view]) |
| landing_signup_btn.click(fn=show_auth, outputs=[landing_view, auth_view, dash_view]) |
| cta_start.click(fn=show_auth, outputs=[landing_view, auth_view, dash_view]) |
| auth_back.click(fn=show_landing, outputs=[landing_view, auth_view, dash_view]) |
| |
| auth_submit.click( |
| fn=handle_auth, |
| inputs=[auth_email, auth_pass], |
| outputs=[user_state, auth_msg, landing_view, auth_view, dash_view] |
| ).then( |
| fn=show_dashboard, inputs=[user_state], outputs=[landing_view, auth_view, dash_view, stat_total, stat_amount, stat_dupes, stat_conf] |
| ).then( |
| fn=get_user_invoices, inputs=[user_state], outputs=[invoices_table] |
| ) |
| |
| def logout(): |
| return None, *show_landing(), *["<div class='stat-card'><div class='stat-label'>Total Invoices</div><div class='stat-value'>0</div></div>"]*4 |
| |
| dash_logout.click(fn=logout, outputs=[user_state, landing_view, auth_view, dash_view, stat_total, stat_amount, stat_dupes, stat_conf]) |
|
|
| def switch_panel(panel_name): |
| u, i = gr.update(visible=False), gr.update(visible=False) |
| if panel_name == "upload": u = gr.update(visible=True) |
| elif panel_name == "invoices": i = gr.update(visible=True) |
| return u, i |
|
|
| dash_nav_upload.click(fn=lambda: switch_panel("upload"), outputs=[panel_upload, panel_invoices]) |
| dash_nav_inv.click(fn=lambda: switch_panel("invoices"), outputs=[panel_upload, panel_invoices]) |
|
|
| def handle_processing(files, user): |
| status, df = handle_upload(files, user) |
| total, amount, dupes, conf = get_dashboard_stats(user) |
| return status, df, ( |
| f"<div class='stat-card'><div class='stat-label'>Total Invoices</div><div class='stat-value'>{total}</div></div>", |
| f"<div class='stat-card'><div class='stat-label'>Total Amount</div><div class='stat-value'>{amount}</div></div>", |
| f"<div class='stat-card'><div class='stat-label'>Duplicates</div><div class='stat-value'>{dupes}</div></div>", |
| f"<div class='stat-card'><div class='stat-label'>Avg Confidence</div><div class='stat-value'>{conf}</div></div>" |
| ) |
|
|
| process_btn.click(fn=handle_processing, inputs=[file_input, user_state], outputs=[upload_status, invoices_table, stat_total, stat_amount, stat_dupes, stat_conf]) |
| |
| def handle_export(user_dict): |
| df = get_user_invoices(user_dict) |
| if df.empty: return None |
| temp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv") |
| df.to_csv(temp.name, index=False) |
| return gr.update(value=temp.name, visible=True) |
| export_btn.click(fn=handle_export, inputs=[user_state], outputs=[csv_file]) |
|
|
| up_basic.click(fn=lambda u: create_stripe_checkout("basic", u), inputs=[user_state], outputs=[checkout_link]) |
| up_pro.click(fn=lambda u: create_stripe_checkout("pro", u), inputs=[user_state], outputs=[checkout_link]) |
|
|
| if __name__ == "__main__": |
| app.launch(server_name="0.0.0.0", server_port=7860) |