Spaces:
Sleeping
Sleeping
| 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 += "<details><summary><b style='cursor:pointer;'>⚠️ ATS varování (Klikni pro rozbalení)</b></summary>\n\n" | |
| for w in analysis.get("ats_warnings", []): | |
| md += f"- {w}\n" | |
| md += "</details>\n\n" | |
| md += "<details><summary><b style='cursor:pointer;'>💼 LinkedIn headline nápady (Klikni pro rozbalení)</b></summary>\n\n" | |
| for h in analysis.get("linkedin_headline_ideas", []): | |
| md += f"- {h}\n" | |
| md += "</details>\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 "<div style='color:red;padding:10px;'>❌ Nesprávné heslo administrátora.</div>" | |
| if not email or "@" not in email: | |
| return "<div style='color:red;padding:10px;'>❌ Zadej platný e-mail pro simulaci doručení.</div>" | |
| 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""" | |
| <div style="border: 2px dashed #cbd5e1; padding: 16px; border-radius: 12px; background: #f8fafc; margin-top: 15px;"> | |
| <div style="background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden;"> | |
| <div style="background: #1e3a8a; color: white; padding: 12px 16px; font-weight: bold; font-size: 14px; display: flex; justify-content: space-between;"> | |
| <span>📧 Doručená pošta pro: {email}</span> | |
| <span style="opacity: 0.7;">Právě teď</span> | |
| </div> | |
| <div style="padding: 20px; font-family: sans-serif; color: #334155; line-height: 1.5;"> | |
| <h3 style="margin-top: 0; color: #1e293b;">Děkujeme za nákup na ResumeLens AI! 🎉</h3> | |
| <p>Tvůj platební příkaz pro <strong>{plan_title}</strong> byl úspěšně zpracován.</p> | |
| <p style="margin-bottom: 4px;">Zde je tvůj osobní licenční klíč:</p> | |
| <div style="background: #f1f5f9; border: 1px solid #cbd5e1; padding: 12px; border-radius: 6px; font-family: monospace; font-size: 16px; font-weight: bold; color: #0f172a; text-align: center; margin: 15px 0; letter-spacing: 0.5px;"> | |
| {key} | |
| </div> | |
| <p style="font-size: 13px; color: #64748b;">Vlož tento klíč do aplikace v sekci "Máš Pro licenci?" pro odemčení všech funkcí.</p> | |
| <hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;" /> | |
| <p style="font-size: 12px; color: #94a3b8; margin: 0;">Tento e-mail byl automaticky generován platební bránou Stripe.</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return email_html | |
| # ─── HTML komponenty pro UI ────────────────────────────────────── | |
| def free_upgrade_html(): | |
| return f""" | |
| <div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:12px;padding:16px;margin-top:8px"> | |
| <p style="margin:0 0 8px;font-weight:600;color:#0369a1">🚀 Free plán: 3 analýzy/hodinu</p> | |
| <p style="margin:0 0 12px;color:#475569;font-size:14px">Upgraduj pro neomezený přístup, historii hledání a čisté PDF bez watermarku.</p> | |
| <a href="{STRIPE_SINGLE}" target="_blank" style="background:#0ea5e9;color:white;padding:8px 16px;border-radius:8px;text-decoration:none;margin-right:8px;font-size:14px;font-weight:500;">Jednorázová analýza — $2.99</a> | |
| <a href="{STRIPE_PRO}" target="_blank" style="background:#7c3aed;color:white;padding:8px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;">Pro plán — $9.99/měsíc</a> | |
| </div>""" | |
| def upgrade_html(): | |
| return f""" | |
| <div style="background:#fef3c7;border:1px solid #fcd34d;border-radius:12px;padding:16px;margin-top:8px"> | |
| <p style="margin:0 0 8px;font-weight:600;color:#92400e">⏳ Limit dosažen</p> | |
| <p style="margin:0 0 12px;color:#475569;font-size:14px">3 analýzy za hodinu vyčerpány. Upgraduj nebo počkej hodinu.</p> | |
| <a href="{STRIPE_PRO}" target="_blank" style="background:#7c3aed;color:white;padding:8px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;">Upgrade na Pro — $9.99/měsíc</a> | |
| </div>""" | |
| # ─── 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(""" | |
| <div style="background:#fef3c7;border:1px solid #f59e0b;text-align:center;padding:6px;font-size:13px;font-weight:600;color:#92400e"> | |
| ⚠️ SANDBOX MODUS — Simulátor nákupů a Stripe testování aktivní | |
| </div>""") | |
| gr.HTML(""" | |
| <div style="text-align:center;padding:24px 0 8px"> | |
| <h1 style="font-size:2rem;font-weight:700;color:#1e293b;margin:0">🎯 ResumeLens AI</h1> | |
| <p style="color:#64748b;margin:8px 0 0;font-size:1.1rem"> | |
| Upload your CV. Paste the job. Know exactly what to fix — in your own words. | |
| </p> | |
| </div>""") | |
| 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 | |
| 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() |