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()