Download app.py from simikkk/test4: direct link, hf CLI and curl.
- Browser
- Download file 24.2 kB
-
https://huggingface.co/spaces/simikkk/test4/resolve/main/app.py
- Command line
-
hf download hf://spaces/simikkk/test4/app.py
-
curl -L -o app.py https://huggingface.co/spaces/simikkk/test4/resolve/main/app.py
24.2 kB
| 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 | |
| # ========================================== | |
| # 1. KONFIGURACE A BEZPEČNOST | |
| # ========================================== | |
| 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')) | |
| # ========================================== | |
| # 2. AI PIPELINE | |
| # ========================================== | |
| 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"} | |
| # ========================================== | |
| # 3. MODERN SAAS DESIGN SYSTEM (CSS) | |
| # ========================================== | |
| 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; } | |
| """ | |
| # ========================================== | |
| # 4. BACKEND LOGICS | |
| # ========================================== | |
| 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}" | |
| # ========================================== | |
| # 5. GRADIO UI ARCHITECTURE | |
| # ========================================== | |
| 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>' | |
| # ---------------- VIEW 1: LANDING ---------------- | |
| 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> | |
| """) | |
| # ---------------- VIEW 2: AUTH ---------------- | |
| 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") | |
| # ---------------- VIEW 3: DASHBOARD ---------------- | |
| 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"): | |
| # Stat Cards Row | |
| 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>") | |
| # Upload Panel | |
| 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("") | |
| # Invoices Panel | |
| 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("") | |
| # ========================================== | |
| # 6. EVENT BINDINGS | |
| # ========================================== | |
| 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) | |
| # Update stats on login | |
| 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) |