import gradio as gr import os import time from collections import defaultdict from fastapi import Request, Response from pdf_parser import extract_text_from_pdf from analyzer import analyze_cv from report_generator import generate_pdf_report from storage import check_license_key from webhook import handle_stripe_webhook # ─── Konfigurace ──────────────────────────────────────────────── STRIPE_SINGLE = os.environ.get("STRIPE_LINK_SINGLE", "#") STRIPE_PRO = os.environ.get("STRIPE_LINK_PRO", "#") SANDBOX_PASSWORD = os.environ.get("SANDBOX_PASSWORD", "admin") # Výchozí heslo pro test IS_SANDBOX = os.environ.get("STRIPE_MODE", "sandbox") == "sandbox" # ─── Rate limiting ─────────────────────────────────────────────── request_log = defaultdict(list) def check_rate_limit(session_id: str) -> bool: now = time.time() request_log[session_id] = [t for t in request_log[session_id] if now - t < 3600] if len(request_log[session_id]) >= 3: return False request_log[session_id].append(now) return True # ─── Hlavní analýza ───────────────────────────────────────────── def run_analysis(cv_file, cv_text_input, job_text, lang, license_key, session_id): cv_text = "" if cv_file is not None: cv_text = extract_text_from_pdf(cv_file) if not cv_text and cv_text_input: cv_text = cv_text_input.strip() if not cv_text: return "❌ Nahraj CV nebo vlož text.", None, free_upgrade_html(), gr.update(visible=False) if not job_text or len(job_text) < 50: return "❌ Vlož popis pozice (alespoň 50 znaků).", None, free_upgrade_html(), gr.update(visible=False) license_info = check_license_key(license_key) is_pro = license_info is not None and license_info.get("active") if not is_pro: if not check_rate_limit(session_id): return "⚠️ Limit 3 analýzy/hodinu vyčerpán. Upgraduj nebo počkej.", None, upgrade_html(), gr.update(visible=False) analysis = analyze_cv(cv_text, job_text, language=lang) if "error" in analysis: return f"❌ {analysis['error']}", None, "", gr.update(visible=False) score = analysis.get("match_score", 0) emoji = "🟢" if score >= 75 else "🟡" if score >= 50 else "🔴" # Sestavení hlavního výstupu md = f"## {emoji} Match Score: {score}/100\n{analysis.get('score_explanation', '')}\n\n---\n\n" md += "### 🚀 Kariérní doporučení & Úroveň\n" md += f"{analysis.get('career_recommendations', 'Nebylo vygenerováno.')}\n\n---\n\n" md += "### 🎯 Tvoje 3 nejdůležitější akce\n" for i, a in enumerate(analysis.get("top_3_actions", []), 1): md += f"\n**{i}.** {a}" md += "\n\n---\n\n### 🔑 Chybějící klíčová slova\n" for kw in analysis.get("missing_keywords", []): if isinstance(kw, dict): md += f"\n- **{kw.get('keyword', '')}** — {kw.get('why_it_matters', '')}" else: md += f"\n- {kw}" md += "\n\n---\n\n### ✍️ Co posílit (přepiš vlastními slovy)\n" for sec in analysis.get("weak_sections", []): if isinstance(sec, dict): md += f"\n> *\"{sec.get('original_phrase', '')}\"*\n" md += f"- **Problém:** {sec.get('problem', '')}\n" md += f"- **Co sdělit:** {sec.get('what_to_convey_instead', '')}\n" md += f"- **Směr:** {sec.get('example_direction', '')}\n" # Rozbalovací sekce md += "\n\n---\n\n" md += "
⚠️ ATS varování (Klikni pro rozbalení)\n\n" for w in analysis.get("ats_warnings", []): md += f"- {w}\n" md += "
\n\n" md += "
💼 LinkedIn headline nápady (Klikni pro rozbalení)\n\n" for h in analysis.get("linkedin_headline_ideas", []): md += f"- {h}\n" md += "
\n\n" pdf_path = generate_pdf_report(analysis, is_pro=is_pro) # Pokud je PRO, aktivujeme v UI zobrazení historie history_update = gr.update(visible=True) if is_pro else gr.update(visible=False) return md, pdf_path, "" if is_pro else free_upgrade_html(), history_update # ─── Sandbox simulace nákupu a e-mailu ────────────────────────── def sandbox_simulate_payment(password: str, email: str, plan: str): if not SANDBOX_PASSWORD or password != SANDBOX_PASSWORD: return "
❌ Nesprávné heslo administrátora.
" if not email or "@" not in email: return "
❌ Zadej platný e-mail pro simulaci doručení.
" from storage import create_license_key key = create_license_key(email, plan) plan_title = "Pro Plan (Měsíční předplatné)" if plan == "pro" else "Jednorázová analýza" # HTML šablona e-mailu, který reálně zákazníkovi přijde email_html = f"""
📧 Doručená pošta pro: {email} Právě teď

Děkujeme za nákup na ResumeLens AI! 🎉

Tvůj platební příkaz pro {plan_title} byl úspěšně zpracován.

Zde je tvůj osobní licenční klíč:

{key}

Vlož tento klíč do aplikace v sekci "Máš Pro licenci?" pro odemčení všech funkcí.


Tento e-mail byl automaticky generován platební bránou Stripe.

""" return email_html # ─── HTML komponenty pro UI ────────────────────────────────────── def free_upgrade_html(): return f"""

🚀 Free plán: 3 analýzy/hodinu

Upgraduj pro neomezený přístup, historii hledání a čisté PDF bez watermarku.

Jednorázová analýza — $2.99 Pro plán — $9.99/měsíc
""" def upgrade_html(): return f"""

⏳ Limit dosažen

3 analýzy za hodinu vyčerpány. Upgraduj nebo počkej hodinu.

Upgrade na Pro — $9.99/měsíc
""" # ─── Gradio UI Layout ──────────────────────────────────────────── with gr.Blocks( theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"), title="ResumeLens AI" ) as demo: session_id = gr.State(lambda: str(time.time())) if IS_SANDBOX: gr.HTML("""
⚠️ SANDBOX MODUS — Simulátor nákupů a Stripe testování aktivní
""") gr.HTML("""

🎯 ResumeLens AI

Upload your CV. Paste the job. Know exactly what to fix — in your own words.

""") with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 1️⃣ Tvoje CV a Nastavení") lang_choice = gr.Radio(["cs", "en", "de"], label="Jazyk reportu", value="cs") cv_file = gr.File(label="Nahraj PDF", file_types=[".pdf"]) cv_text_input = gr.Textbox(label="nebo vlož text CV", placeholder="Sem vlož obsah CV..." , lines=4) with gr.Accordion("🔑 Máš Pro licenci? Zadej klíč", open=False): license_key = gr.Textbox(label="Licenční klíč", placeholder="rl_xxxxxxxxxxxxxxxx", type="password") gr.Markdown(f"Nemáš klíč? [Kup si přístup zde]({STRIPE_PRO})") analyze_btn = gr.Button("🔍 Analyzovat CV", variant="primary", size="lg") with gr.Column(scale=1): gr.Markdown("### 2️⃣ Popis pozice") job_text = gr.Textbox(label="Popis pracovní nabídky", placeholder="Zkopíruj celý popis pozice z Jobsu/LinkedInu...", lines=15) gr.Markdown("---") # Sekce pro zobrazení historie pro platící zákazníky (skrytá pro free) with gr.Row(visible=False) as history_row: with gr.Column(): gr.Markdown("### 🕒 Moje uložená historie (Pouze PRO)") gr.Dataframe( headers=["Datum", "Pozice", "Dosažené Skóre", "Status"], value=[ ["16. 06. 2026", "Senior Marketing Manager", "28/100", "Slabá shoda"], ["15. 06. 2026", "Social Media Specialist", "82/100", "Skvělá shoda! 🔥"] ], interactive=False ) gr.Markdown("---") with gr.Row(): with gr.Column(scale=2): result_output = gr.Markdown() with gr.Column(scale=1): pdf_output = gr.File(label="📄 Stáhnout PDF report") upgrade_output = gr.HTML() analyze_btn.click( fn=run_analysis, inputs=[cv_file, cv_text_input, job_text, lang_choice, license_key, session_id], outputs=[result_output, pdf_output, upgrade_output, history_row] ) # ─── Nástroje pro vývojáře (Simulátor Stripe checkoutu) ──────── with gr.Accordion("🛒 Simulátor nákupu (Testovací rozhraní pro Stripe & E-mail)", open=False): gr.Markdown("Tato sekce simuluje přesně to, co se stane, když uživatel zaplatí na Stripe. Vygeneruje licenční e-mail.") with gr.Row(): sandbox_pw = gr.Textbox(label="Heslo administrátora", type="password", value="admin") sandbox_email = gr.Textbox(label="E-mail fiktivního zákazníka", placeholder="tvuj-mail@seznam.cz") sandbox_plan = gr.Radio(["single", "pro"], label="Výběr produktu na Stripe", value="pro") sandbox_btn = gr.Button("💳 Simulovat úspěšnou platbu na Stripe", variant="secondary") sandbox_result = gr.HTML(label="Doručená pošta zákazníka") sandbox_btn.click( fn=sandbox_simulate_payment, inputs=[sandbox_pw, sandbox_email, sandbox_plan], outputs=[sandbox_result] ) # ─── FastAPI webhook endpoint ──────────────────────────────────── app = demo.app @app.post("/stripe-webhook") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("stripe-signature", "") result = handle_stripe_webhook(payload, sig_header) status = 200 if result["ok"] else 400 return Response(status_code=status) if __name__ == "__main__": demo.launch()