Spaces:
Sleeping
Sleeping
File size: 13,284 Bytes
8304a2c eb1c22d 8304a2c eb1c22d a4b1180 eb1c22d 8304a2c eb1c22d 8304a2c 8fe842c 8304a2c a4b1180 8304a2c a4b1180 8304a2c a4b1180 8304a2c 8fe842c 8304a2c a4b1180 8304a2c a4b1180 8304a2c 8fe842c 8304a2c a4b1180 8304a2c 6e8d6c8 a4b1180 6e8d6c8 8304a2c 6e8d6c8 8304a2c a4b1180 8fe842c 8304a2c a4b1180 8fe842c 8304a2c 8fe842c 8304a2c a4b1180 8fe842c 8304a2c a4b1180 8304a2c a4b1180 8304a2c a4b1180 8304a2c a4b1180 8304a2c a4b1180 8304a2c 2187522 8304a2c a4b1180 8304a2c 2187522 8304a2c 2187522 a4b1180 8304a2c a4b1180 2187522 8304a2c 2187522 a4b1180 2187522 8304a2c 2187522 8fe842c 2187522 a4b1180 8fe842c a4b1180 8fe842c 2187522 a4b1180 2187522 8fe842c a4b1180 2187522 8fe842c 6e8d6c8 2187522 8fe842c a4b1180 2187522 a4b1180 2187522 8fe842c 2187522 a4b1180 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | 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() |