ResumeLens / app.py
simikkk's picture
Update app.py
a4b1180 verified
Raw
History Blame Contribute Delete
13.3 kB
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
@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()