Spaces:
Running
Running
| """ | |
| CV + Portfolio Analyzer — Python/Streamlit | |
| Powered by Claude & Gemini · Multi-step Tool Use | |
| """ | |
| import json | |
| import os | |
| import streamlit as st | |
| import anthropic | |
| import google.generativeai as genai | |
| from google.generativeai.types import FunctionDeclaration, Tool as GeminiTool | |
| # ─── Page Config ─────────────────────────────────────────────────────────────── | |
| st.set_page_config( | |
| page_title="CV + Portfolio Analyzer", | |
| page_icon="📄", | |
| layout="wide", | |
| ) | |
| # ─── Custom CSS ──────────────────────────────────────────────────────────────── | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| html, body, [data-testid="stAppViewContainer"] { | |
| background: #0a0f1a !important; | |
| color: #e2e8f0 !important; | |
| font-family: 'Inter', -apple-system, sans-serif !important; | |
| } | |
| [data-testid="stHeader"] { background: transparent !important; } | |
| /* Hide streamlit default menu */ | |
| #MainMenu, footer, header { visibility: hidden; } | |
| /* Sidebar */ | |
| [data-testid="stSidebar"] { display: none !important; } | |
| /* Text areas */ | |
| textarea { | |
| background: #ffffff !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| color: #000000 !important; | |
| border-radius: 12px !important; | |
| font-family: 'SF Mono', 'Fira Code', monospace !important; | |
| font-size: 13px !important; | |
| } | |
| /* Text inputs */ | |
| input[type="text"], input[type="password"] { | |
| background: rgba(255,255,255,0.04) !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| color: #e2e8f0 !important; | |
| border-radius: 8px !important; | |
| } | |
| /* Buttons */ | |
| .stButton > button { | |
| background: linear-gradient(135deg, #f59e0b, #ef4444) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 12px !important; | |
| font-weight: 600 !important; | |
| padding: 12px 40px !important; | |
| font-size: 15px !important; | |
| transition: transform 0.2s !important; | |
| box-shadow: 0 4px 24px rgba(245,158,11,0.3) !important; | |
| } | |
| .stButton > button:hover { transform: translateY(-2px) !important; } | |
| /* Tabs */ | |
| [data-testid="stTabs"] button { | |
| background: transparent !important; | |
| color: #64748b !important; | |
| border-radius: 9px !important; | |
| font-size: 13px !important; | |
| } | |
| [data-testid="stTabs"] button[aria-selected="true"] { | |
| background: rgba(245,158,11,0.15) !important; | |
| color: #f59e0b !important; | |
| border-bottom: 2px solid #f59e0b !important; | |
| } | |
| /* Metrics */ | |
| [data-testid="stMetric"] { | |
| background: rgba(255,255,255,0.04) !important; | |
| border: 1px solid rgba(255,255,255,0.08) !important; | |
| border-radius: 14px !important; | |
| padding: 16px !important; | |
| } | |
| [data-testid="stMetricValue"] { color: white !important; font-size: 28px !important; } | |
| [data-testid="stMetricLabel"] { color: #64748b !important; font-size: 12px !important; } | |
| /* Progress bar */ | |
| .stProgress > div > div { | |
| background: linear-gradient(90deg, #f59e0b, #ef4444) !important; | |
| border-radius: 4px !important; | |
| } | |
| /* Expanders */ | |
| [data-testid="stExpander"] { | |
| background: rgba(255,255,255,0.03) !important; | |
| border: 1px solid rgba(255,255,255,0.07) !important; | |
| border-radius: 12px !important; | |
| } | |
| /* Divider */ | |
| hr { border-color: rgba(255,255,255,0.06) !important; } | |
| /* Labels */ | |
| label { color: #94a3b8 !important; font-size: 12px !important; font-weight: 600 !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ─── Tool Definitions ───────────────────────────────────────────────────────── | |
| TOOLS = [ | |
| { | |
| "name": "analyze_cv_sections", | |
| "description": "Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"}, | |
| "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"}, | |
| "section_scores": { | |
| "type": "object", | |
| "description": "Quality score per section (0-10)", | |
| "additionalProperties": {"type": "number"} | |
| }, | |
| "overall_score": {"type": "number", "description": "CV strength score (0-100)"}, | |
| "key_strengths": {"type": "array", "items": {"type": "string"}}, | |
| "critical_gaps": {"type": "array", "items": {"type": "string"}}, | |
| "recommendations": {"type": "array", "items": {"type": "string"}} | |
| }, | |
| "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"] | |
| } | |
| }, | |
| { | |
| "name": "calculate_job_match", | |
| "description": "Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "match_score": {"type": "number", "description": "Match percentage 0-100"}, | |
| "matched_skills": {"type": "array", "items": {"type": "string"}}, | |
| "missing_skills": {"type": "array", "items": {"type": "string"}}, | |
| "experience_match": {"type": "string"}, | |
| "highlight_points": {"type": "array", "items": {"type": "string"}, "description": "Strong alignment points to emphasize"}, | |
| "improvement_tips": {"type": "array", "items": {"type": "string"}} | |
| }, | |
| "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"] | |
| } | |
| }, | |
| { | |
| "name": "write_cover_letter", | |
| "description": "Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.", | |
| "input_schema": { | |
| "type": "object", | |
| "properties": { | |
| "subject_line": {"type": "string"}, | |
| "cover_letter": {"type": "string", "description": "Full formatted cover letter"}, | |
| "key_selling_points": {"type": "array", "items": {"type": "string"}} | |
| }, | |
| "required": ["cover_letter", "key_selling_points"] | |
| } | |
| } | |
| ] | |
| # ─── Gemini Tool Definitions (function declarations) ────────────────────────── | |
| GEMINI_TOOLS = [GeminiTool(function_declarations=[ | |
| FunctionDeclaration( | |
| name="analyze_cv_sections", | |
| description="Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"}, | |
| "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"}, | |
| "overall_score": {"type": "number", "description": "CV strength score (0-100)"}, | |
| "key_strengths": {"type": "array", "items": {"type": "string"}}, | |
| "critical_gaps": {"type": "array", "items": {"type": "string"}}, | |
| "recommendations": {"type": "array", "items": {"type": "string"}}, | |
| }, | |
| "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"], | |
| }, | |
| ), | |
| FunctionDeclaration( | |
| name="calculate_job_match", | |
| description="Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "match_score": {"type": "number", "description": "Match percentage 0-100"}, | |
| "matched_skills": {"type": "array", "items": {"type": "string"}}, | |
| "missing_skills": {"type": "array", "items": {"type": "string"}}, | |
| "experience_match": {"type": "string"}, | |
| "highlight_points": {"type": "array", "items": {"type": "string"}}, | |
| "improvement_tips": {"type": "array", "items": {"type": "string"}}, | |
| }, | |
| "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"], | |
| }, | |
| ), | |
| FunctionDeclaration( | |
| name="write_cover_letter", | |
| description="Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.", | |
| parameters={ | |
| "type": "object", | |
| "properties": { | |
| "subject_line": {"type": "string"}, | |
| "cover_letter": {"type": "string", "description": "Full formatted cover letter"}, | |
| "key_selling_points": {"type": "array", "items": {"type": "string"}}, | |
| }, | |
| "required": ["cover_letter", "key_selling_points"], | |
| }, | |
| ), | |
| ])] | |
| STEPS = [ | |
| {"id": "analyze_cv_sections", "label": "CV Analizi", "icon": "▣", "desc": "Bölümler, güçlü yanlar ve eksikler tespit ediliyor"}, | |
| {"id": "calculate_job_match", "label": "İş Eşleştirme", "icon": "◎", "desc": "Pozisyona uyum oranı hesaplanıyor"}, | |
| {"id": "write_cover_letter", "label": "Ön Yazı", "icon": "✦", "desc": "Kişiselleştirilmiş cover letter oluşturuluyor"}, | |
| ] | |
| # ─── Sample Data ─────────────────────────────────────────────────────────────── | |
| SAMPLE_CV = """John Doe | |
| john@example.com | +1 (555) 123-4567 | linkedin.com/in/johndoe | github.com/johndoe | |
| SUMMARY | |
| Full-stack developer with 4 years of experience building scalable web applications. Passionate about clean code, performance optimization, and developer experience. | |
| EXPERIENCE | |
| Senior Frontend Developer — TechCorp (2022–Present) | |
| - Built React/TypeScript dashboard used by 50k daily users | |
| - Reduced page load time by 40% through code splitting and lazy loading | |
| - Mentored 3 junior developers | |
| Frontend Developer — StartupXYZ (2020–2022) | |
| - Developed e-commerce platform with Next.js and Node.js | |
| - Integrated Stripe payment system and REST APIs | |
| EDUCATION | |
| B.Sc. Computer Science — State University (2016–2020) | |
| SKILLS | |
| JavaScript, TypeScript, React, Next.js, Node.js, PostgreSQL, Docker, AWS""" | |
| SAMPLE_JD = """Senior Full-Stack Engineer — FinTech Startup | |
| We're looking for an experienced full-stack engineer to join our growing team. | |
| Requirements: | |
| - 5+ years of full-stack development experience | |
| - Strong proficiency in React, TypeScript, and Node.js | |
| - Experience with microservices architecture | |
| - Knowledge of financial systems or payment processing (Stripe, Plaid) | |
| - GraphQL API design and implementation | |
| - AWS/GCP cloud infrastructure experience | |
| - Leadership and mentoring skills | |
| - Strong communication and teamwork""" | |
| # ─── Helper: colored badge ───────────────────────────────────────────────────── | |
| def badge(text: str, color: str = "#f59e0b", bg: str = "rgba(245,158,11,0.12)") -> str: | |
| return ( | |
| f'<span style="display:inline-block;padding:3px 10px;border-radius:20px;' | |
| f'font-size:12px;background:{bg};color:{color};' | |
| f'border:1px solid {color}33;white-space:nowrap;margin:2px">{text}</span>' | |
| ) | |
| def green_badge(t): return badge(t, "#34d399", "rgba(16,185,129,0.12)") | |
| def red_badge(t): return badge(t, "#f87171", "rgba(239,68,68,0.12)") | |
| def amber_badge(t): return badge(t, "#fbbf24", "rgba(245,158,11,0.12)") | |
| def blue_badge(t): return badge(t, "#60a5fa", "rgba(59,130,246,0.12)") | |
| def score_circle(score: int, label: str, color: str = "#f59e0b"): | |
| pct = max(0, min(100, score)) | |
| return f""" | |
| <div style="text-align:center;padding:16px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:14px;"> | |
| <svg width="80" height="80" style="transform:rotate(-90deg)"> | |
| <circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="6"/> | |
| <circle cx="40" cy="40" r="34" fill="none" stroke="{color}" | |
| stroke-width="6" stroke-linecap="round" | |
| stroke-dasharray="{pct/100*213.6:.1f} {213.6 - pct/100*213.6:.1f}" | |
| style="transition:stroke-dasharray 1s ease"/> | |
| <text x="40" y="40" text-anchor="middle" dominant-baseline="central" | |
| fill="white" font-size="15" font-weight="600" | |
| style="transform:rotate(90deg) translate(0px,-80px)">{score}</text> | |
| </svg> | |
| <div style="color:#64748b;font-size:12px;margin-top:6px">{label}</div> | |
| <div style="color:white;font-size:20px;font-weight:700">{score}/100</div> | |
| </div>""" | |
| # ─── Core: run analysis ───────────────────────────────────────────────────────── | |
| def run_analysis(cv_text: str, job_desc: str, api_key: str): | |
| """Generator that yields status dicts as Claude streams tool results.""" | |
| client = anthropic.Anthropic(api_key=api_key) | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": ( | |
| "You are an expert career coach and CV analyzer. " | |
| "Analyze the following CV and job description thoroughly using all three available tools in sequence.\n\n" | |
| f"<cv>\n{cv_text}\n</cv>\n\n" | |
| f"<job_description>\n{job_desc}\n</job_description>\n\n" | |
| "Please:\n" | |
| "1. First call analyze_cv_sections to evaluate the CV quality\n" | |
| "2. Then call calculate_job_match to assess alignment with the job\n" | |
| "3. Finally call write_cover_letter to create a compelling application letter\n\n" | |
| "Use all three tools." | |
| ) | |
| } | |
| ] | |
| results = {"cv": None, "match": None, "letter": None} | |
| tool_log = [] | |
| continue_loop = True | |
| while continue_loop: | |
| yield {"type": "status", "msg": "Claude API'ye istek gönderiliyor..."} | |
| response = client.messages.create( | |
| model="claude-opus-4-5", | |
| max_tokens=4000, | |
| tools=TOOLS, | |
| messages=messages, | |
| ) | |
| messages.append({"role": "assistant", "content": response.content}) | |
| tool_use_blocks = [b for b in response.content if b.type == "tool_use"] | |
| if not tool_use_blocks: | |
| continue_loop = False | |
| break | |
| tool_results_msg = [] | |
| for block in tool_use_blocks: | |
| tool_name = block.name | |
| tool_input = block.input | |
| step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1) | |
| step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name | |
| yield {"type": "step", "step_idx": step_idx, "label": step_label} | |
| yield {"type": "status", "msg": f"{step_label} çalışıyor..."} | |
| # Store results | |
| if tool_name == "analyze_cv_sections": | |
| results["cv"] = tool_input | |
| elif tool_name == "calculate_job_match": | |
| results["match"] = tool_input | |
| elif tool_name == "write_cover_letter": | |
| results["letter"] = tool_input | |
| tool_log.append({"name": tool_name, "result": tool_input}) | |
| yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log} | |
| tool_results_msg.append({ | |
| "type": "tool_result", | |
| "tool_use_id": block.id, | |
| "content": json.dumps(tool_input), | |
| }) | |
| messages.append({"role": "user", "content": tool_results_msg}) | |
| if response.stop_reason == "end_turn": | |
| continue_loop = False | |
| yield {"type": "done", "results": results, "log": tool_log} | |
| # ─── Core: run analysis with Gemini ──────────────────────────────────────────── | |
| def run_analysis_gemini(cv_text: str, job_desc: str, api_key: str): | |
| """Generator that yields status dicts as Gemini streams tool results.""" | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel( | |
| model_name="gemini-2.0-flash", | |
| tools=GEMINI_TOOLS, | |
| system_instruction=( | |
| "You are an expert career coach and CV analyzer. " | |
| "Use all three available tools in sequence: " | |
| "first analyze_cv_sections, then calculate_job_match, then write_cover_letter." | |
| ), | |
| ) | |
| user_prompt = ( | |
| f"Analyze this CV and job description using all three tools in order.\n\n" | |
| f"<cv>\n{cv_text}\n</cv>\n\n" | |
| f"<job_description>\n{job_desc}\n</job_description>\n\n" | |
| "1. Call analyze_cv_sections\n" | |
| "2. Call calculate_job_match\n" | |
| "3. Call write_cover_letter\n" | |
| "Use all three tools." | |
| ) | |
| results = {"cv": None, "match": None, "letter": None} | |
| tool_log = [] | |
| history = [] | |
| # Ordered list of tools to call | |
| tools_to_call = ["analyze_cv_sections", "calculate_job_match", "write_cover_letter"] | |
| current_prompt = user_prompt | |
| for expected_tool in tools_to_call: | |
| yield {"type": "status", "msg": "Gemini API'ye istek gönderiliyor..."} | |
| chat = model.start_chat(history=history) | |
| response = chat.send_message(current_prompt) | |
| # Find function call in response | |
| fc = None | |
| for part in response.parts: | |
| if hasattr(part, "function_call") and part.function_call.name: | |
| fc = part.function_call | |
| break | |
| if fc is None: | |
| # Try to extract JSON from text as fallback | |
| break | |
| tool_name = fc.name | |
| # Convert Gemini MapComposite to plain dict | |
| tool_input = dict(fc.args) | |
| # Recursively convert nested MapComposite / ListValue objects | |
| tool_input = json.loads(json.dumps(tool_input, default=str)) | |
| # De-nest arrays stored as {"values": [...]} | |
| for k, v in tool_input.items(): | |
| if isinstance(v, dict) and list(v.keys()) == ["values"]: | |
| tool_input[k] = v["values"] | |
| step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1) | |
| step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name | |
| yield {"type": "step", "step_idx": step_idx, "label": step_label} | |
| yield {"type": "status", "msg": f"{step_label} çalışıyor..."} | |
| if tool_name == "analyze_cv_sections": | |
| results["cv"] = tool_input | |
| elif tool_name == "calculate_job_match": | |
| results["match"] = tool_input | |
| elif tool_name == "write_cover_letter": | |
| results["letter"] = tool_input | |
| tool_log.append({"name": tool_name, "result": tool_input}) | |
| yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log} | |
| # Build history for next turn: assistant called the function, we return the result | |
| history = chat.history + [ | |
| { | |
| "role": "user", | |
| "parts": [{"function_response": {"name": tool_name, "response": {"result": str(tool_input)}}}], | |
| } | |
| ] | |
| current_prompt = ( | |
| f"Good. Now call the next tool." | |
| ) | |
| yield {"type": "done", "results": results, "log": tool_log} | |
| # ─── Session State Init ───────────────────────────────────────────────────────── | |
| for key, default in [ | |
| ("stage", "input"), # input | loading | results | |
| ("results", {}), | |
| ("tool_log", []), | |
| ("cv_text", SAMPLE_CV), | |
| ("job_desc", SAMPLE_JD), | |
| ("provider", "Claude"), | |
| ]: | |
| if key not in st.session_state: | |
| st.session_state[key] = default | |
| # ─── Header ──────────────────────────────────────────────────────────────────── | |
| st.markdown(""" | |
| <div style="border-bottom:1px solid rgba(255,255,255,0.06);padding:18px 0 16px 0;margin-bottom:32px;"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;"> | |
| <div style="display:flex;align-items:center;gap:12px;"> | |
| <div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#f59e0b,#ef4444); | |
| display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:white;">CV</div> | |
| <div> | |
| <div style="font-size:16px;font-weight:700;color:white;">CV + Portfolio Analyzer</div> | |
| <div style="font-size:11px;color:#475569;letter-spacing:0.06em;">POWERED BY CLAUDE & GEMINI · MULTI-STEP TOOL USE</div> | |
| </div> | |
| </div> | |
| <div> | |
| """ + | |
| green_badge("Function Calling") + " " + | |
| blue_badge("Context Management") + " " + | |
| amber_badge("Multi-step Reasoning") + | |
| """ | |
| </div> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ─── Settings Expander (HF Spaces uyumlu) ──────────────────────────────── | |
| with st.expander("⚙️ Ayarlar — API Anahtarı & Model Seçimi", expanded=not st.session_state.get("active_key")): | |
| col_radio, col_key, col_info = st.columns([1, 2, 1]) | |
| with col_radio: | |
| provider = st.radio( | |
| "AI Sağlayıcı", | |
| options=["Claude", "Gemini"], | |
| index=0 if st.session_state.provider == "Claude" else 1, | |
| horizontal=False, | |
| help="Hangi AI modeli kullanılsın?" | |
| ) | |
| st.session_state.provider = provider | |
| with col_key: | |
| if provider == "Claude": | |
| api_key = st.text_input( | |
| "Anthropic API Anahtarı", | |
| value=os.environ.get("ANTHROPIC_API_KEY", ""), | |
| type="password", | |
| placeholder="sk-ant-...", | |
| help="https://console.anthropic.com" | |
| ) | |
| gemini_key = "" | |
| else: | |
| api_key = "" | |
| gemini_key = st.text_input( | |
| "Google Gemini API Anahtarı", | |
| value=os.environ.get("GOOGLE_API_KEY", ""), | |
| type="password", | |
| placeholder="AIza...", | |
| help="https://aistudio.google.com/app/apikey" | |
| ) | |
| with col_info: | |
| if provider == "Claude": | |
| st.markdown( | |
| '<div style="padding:8px 12px;border-radius:8px;margin-top:24px;' | |
| 'background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);' | |
| 'font-size:12px;color:#f59e0b">🤖 claude-opus-4-5</div>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| '<div style="padding:8px 12px;border-radius:8px;margin-top:24px;' | |
| 'background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.2);' | |
| 'font-size:12px;color:#60a5fa">🤖 gemini-2.0-flash</div>', | |
| unsafe_allow_html=True | |
| ) | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # STAGE: INPUT | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| if st.session_state.stage == "input": | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| hdr1, btn1 = st.columns([3, 1]) | |
| with hdr1: | |
| st.markdown('<p style="font-size:12px;font-weight:600;color:#94a3b8;letter-spacing:0.08em;text-transform:uppercase">CV / Özgeçmiş</p>', unsafe_allow_html=True) | |
| with btn1: | |
| if st.button("Örnek", key="sample_cv", help="Örnek CV yükle"): | |
| st.session_state.cv_text = SAMPLE_CV | |
| st.rerun() | |
| cv_input = st.text_area("cv_area", value=st.session_state.cv_text, height=360, | |
| label_visibility="collapsed", key="cv_input_area") | |
| with col2: | |
| hdr2, btn2 = st.columns([3, 1]) | |
| with hdr2: | |
| st.markdown('<p style="font-size:12px;font-weight:600;color:#94a3b8;letter-spacing:0.08em;text-transform:uppercase">İş İlanı</p>', unsafe_allow_html=True) | |
| with btn2: | |
| if st.button("Örnek", key="sample_jd", help="Örnek iş ilanı yükle"): | |
| st.session_state.job_desc = SAMPLE_JD | |
| st.rerun() | |
| jd_input = st.text_area("jd_area", value=st.session_state.job_desc, height=360, | |
| label_visibility="collapsed", key="jd_input_area") | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| _, center, _ = st.columns([2, 1, 2]) | |
| with center: | |
| start = st.button("✦ Analizi Başlat", use_container_width=True) | |
| if start: | |
| active_key = api_key if st.session_state.provider == "Claude" else gemini_key | |
| if not active_key: | |
| pname = "Anthropic" if st.session_state.provider == "Claude" else "Google Gemini" | |
| st.error(f"⚠️ Lütfen sol panelden {pname} API anahtarınızı girin.") | |
| elif not cv_input.strip() or not jd_input.strip(): | |
| st.error("Lütfen CV ve iş ilanı alanlarını doldurun.") | |
| else: | |
| st.session_state.cv_text = cv_input | |
| st.session_state.job_desc = jd_input | |
| st.session_state.active_key = active_key | |
| st.session_state.stage = "loading" | |
| st.session_state.results = {} | |
| st.session_state.tool_log = [] | |
| st.rerun() | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # STAGE: LOADING | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| elif st.session_state.stage == "loading": | |
| # Step progress header | |
| step_cols = st.columns(len(STEPS)) | |
| step_placeholders = [] | |
| for i, step in enumerate(STEPS): | |
| with step_cols[i]: | |
| ph = st.empty() | |
| ph.markdown( | |
| f'<div style="text-align:center;padding:12px;border-radius:12px;' | |
| f'background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)">' | |
| f'<div style="font-size:22px">{step["icon"]}</div>' | |
| f'<div style="font-size:13px;font-weight:600;color:#94a3b8;margin-top:6px">{step["label"]}</div>' | |
| f'<div style="font-size:11px;color:#475569;margin-top:4px">{step["desc"]}</div>' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| step_placeholders.append(ph) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| status_ph = st.empty() | |
| progress_ph = st.empty() | |
| log_ph = st.empty() | |
| active_step = 0 | |
| partial_results = {} | |
| log_entries = [] | |
| def render_step(idx, done_steps): | |
| for i, step in enumerate(STEPS): | |
| if i < done_steps: | |
| icon_html = f'<div style="font-size:18px;color:#34d399">✓</div>' | |
| bg = "linear-gradient(135deg,rgba(16,185,129,0.15),rgba(16,185,129,0.05))" | |
| border = "rgba(16,185,129,0.25)" | |
| label_color = "#34d399" | |
| elif i == done_steps: | |
| icon_html = f'<div style="font-size:22px;animation:spin 1s linear infinite">{step["icon"]}</div>' | |
| bg = "rgba(245,158,11,0.08)" | |
| border = "#f59e0b" | |
| label_color = "#f59e0b" | |
| else: | |
| icon_html = f'<div style="font-size:22px;color:#475569">{step["icon"]}</div>' | |
| bg = "rgba(255,255,255,0.03)" | |
| border = "rgba(255,255,255,0.06)" | |
| label_color = "#475569" | |
| step_placeholders[i].markdown( | |
| f'<div style="text-align:center;padding:12px;border-radius:12px;' | |
| f'background:{bg};border:2px solid {border};transition:all 0.4s">' | |
| f'{icon_html}' | |
| f'<div style="font-size:13px;font-weight:600;color:{label_color};margin-top:6px">{step["label"]}</div>' | |
| f'<div style="font-size:11px;color:#475569;margin-top:4px">{step["desc"]}</div>' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| def render_log(entries): | |
| if not entries: | |
| log_ph.markdown( | |
| '<div style="font-family:monospace;font-size:12px;padding:16px;' | |
| 'background:rgba(0,0,0,0.3);border-radius:12px;border:1px solid rgba(255,255,255,0.06);color:#475569">' | |
| 'Tool call log bekleniyor...' | |
| '</div>', unsafe_allow_html=True | |
| ) | |
| return | |
| html = '<div style="font-family:monospace;font-size:12px;padding:16px;background:rgba(0,0,0,0.3);border-radius:12px;border:1px solid rgba(255,255,255,0.06)">' | |
| html += '<div style="font-size:11px;color:#475569;margin-bottom:10px;letter-spacing:0.08em">TOOL CALL LOG</div>' | |
| for e in entries: | |
| preview = json.dumps(e["result"])[:200] + ("..." if len(json.dumps(e["result"])) > 200 else "") | |
| html += f''' | |
| <div style="margin-bottom:12px;padding:12px;background:rgba(255,255,255,0.03);border-radius:8px;border:1px solid rgba(255,255,255,0.06)"> | |
| <div style="color:#f59e0b;font-size:11px;letter-spacing:0.05em;margin-bottom:6px">TOOL CALL → {e["name"]}</div> | |
| <div style="color:#34d399;font-size:11px;margin-bottom:6px">✓ Tamamlandı</div> | |
| <div style="color:#475569;font-size:11px;background:rgba(0,0,0,0.3);padding:8px;border-radius:6px;white-space:pre-wrap;max-height:80px;overflow:hidden">{preview}</div> | |
| </div>''' | |
| html += '</div>' | |
| log_ph.markdown(html, unsafe_allow_html=True) | |
| render_step(0, 0) | |
| render_log([]) | |
| try: | |
| done_count = 0 | |
| _key = st.session_state.get("active_key", "") | |
| _provider = st.session_state.get("provider", "Claude") | |
| _fn = run_analysis if _provider == "Claude" else run_analysis_gemini | |
| for event in _fn(st.session_state.cv_text, st.session_state.job_desc, _key): | |
| if event["type"] == "status": | |
| status_ph.markdown( | |
| f'<div style="text-align:center;color:#94a3b8;font-family:monospace;font-size:13px;padding:8px">' | |
| f'▸ {event["msg"]}</div>', unsafe_allow_html=True | |
| ) | |
| progress_ph.progress(done_count / 3) | |
| elif event["type"] == "step": | |
| render_step(event["step_idx"], done_count) | |
| elif event["type"] == "tool_done": | |
| log_entries.append({"name": event["name"], "result": event["result"]}) | |
| done_count += 1 | |
| partial_results = event["results"] | |
| render_step(done_count, done_count) | |
| render_log(log_entries) | |
| progress_ph.progress(done_count / 3) | |
| elif event["type"] == "done": | |
| st.session_state.results = event["results"] | |
| st.session_state.tool_log = event["log"] | |
| progress_ph.progress(1.0) | |
| status_ph.markdown( | |
| '<div style="text-align:center;color:#34d399;font-family:monospace;font-size:13px;padding:8px">' | |
| '✓ Analiz tamamlandı!</div>', unsafe_allow_html=True | |
| ) | |
| import time; time.sleep(0.5) | |
| st.session_state.stage = "results" | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"Hata: {e}") | |
| if st.button("← Geri Dön"): | |
| st.session_state.stage = "input" | |
| st.rerun() | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| # STAGE: RESULTS | |
| # ════════════════════════════════════════════════════════════════════════════════ | |
| elif st.session_state.stage == "results": | |
| results = st.session_state.results | |
| cv = results.get("cv") or {} | |
| match = results.get("match") or {} | |
| letter = results.get("letter") or {} | |
| tab_overview, tab_match, tab_letter, tab_log = st.tabs( | |
| ["📊 Genel Bakış", "🎯 İş Eşleşmesi", "✉️ Cover Letter", "🔧 Tool Logs"] | |
| ) | |
| # ── Overview Tab ─────────────────────────────────────────────────────────── | |
| with tab_overview: | |
| # Score cards | |
| m1, m2, m3 = st.columns(3) | |
| with m1: | |
| st.markdown(score_circle(int(cv.get("overall_score", 0)), "CV Puanı", "#f59e0b"), unsafe_allow_html=True) | |
| with m2: | |
| st.markdown(score_circle(int(match.get("match_score", 0)), "İş Uyumu", "#10b981"), unsafe_allow_html=True) | |
| with m3: | |
| sections = cv.get("sections_found", []) | |
| badges_html = " ".join(blue_badge(s) for s in sections) | |
| st.markdown( | |
| f'<div style="padding:16px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:14px;min-height:130px">' | |
| f'<div style="font-size:12px;color:#64748b;margin-bottom:10px">Bulunan Bölümler</div>' | |
| f'{badges_html}' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| # Strengths & Gaps | |
| c_str, c_gap = st.columns(2) | |
| with c_str: | |
| items = "".join( | |
| f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#34d399">→</span> {s}</div>' | |
| for s in cv.get("key_strengths", []) | |
| ) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(16,185,129,0.04);border:1px solid rgba(16,185,129,0.12)">' | |
| f'<div style="font-size:13px;font-weight:600;color:#34d399;margin-bottom:12px">✦ Güçlü Yanlar</div>' | |
| f'{items}</div>', unsafe_allow_html=True | |
| ) | |
| with c_gap: | |
| items = "".join( | |
| f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#f87171">→</span> {g}</div>' | |
| for g in cv.get("critical_gaps", []) | |
| ) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(239,68,68,0.04);border:1px solid rgba(239,68,68,0.12)">' | |
| f'<div style="font-size:13px;font-weight:600;color:#f87171;margin-bottom:12px">⚠ Kritik Eksikler</div>' | |
| f'{items}</div>', unsafe_allow_html=True | |
| ) | |
| # Missing sections | |
| missing = cv.get("missing_sections", []) | |
| if missing: | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| badges_html = " ".join(amber_badge(s) for s in missing) | |
| st.markdown( | |
| f'<div style="padding:16px 20px;border-radius:12px;background:rgba(245,158,11,0.06);border:1px solid rgba(245,158,11,0.15)">' | |
| f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:10px">EKSİK BÖLÜMLER</div>' | |
| f'{badges_html}</div>', unsafe_allow_html=True | |
| ) | |
| # Recommendations | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| recs_html = "".join( | |
| f'<div style="display:flex;gap:12px;margin-bottom:10px;padding:10px 14px;border-radius:8px;background:rgba(255,255,255,0.03)">' | |
| f'<span style="min-width:22px;height:22px;border-radius:50%;background:rgba(245,158,11,0.15);color:#f59e0b;' | |
| f'display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700">{i+1}</span>' | |
| f'<span style="font-size:13px;color:#94a3b8;line-height:1.6">{r}</span></div>' | |
| for i, r in enumerate(cv.get("recommendations", [])) | |
| ) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07)">' | |
| f'<div style="font-size:13px;font-weight:600;color:#e2e8f0;margin-bottom:14px">▣ Öneriler</div>' | |
| f'{recs_html}</div>', unsafe_allow_html=True | |
| ) | |
| # ── Match Tab ────────────────────────────────────────────────────────────── | |
| with tab_match: | |
| score = int(match.get("match_score", 0)) | |
| st.markdown( | |
| f'<div style="padding:24px;border-radius:14px;margin-bottom:20px;' | |
| f'background:linear-gradient(135deg,rgba(16,185,129,0.08),rgba(59,130,246,0.08));' | |
| f'border:1px solid rgba(16,185,129,0.15);">' | |
| f'<div style="font-size:32px;font-weight:800;color:white">{score}% Uyum</div>' | |
| f'<div style="font-size:13px;color:#64748b;margin-top:4px">{match.get("experience_match","")}</div>' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| st.progress(score / 100) | |
| c_ok, c_miss = st.columns(2) | |
| with c_ok: | |
| badges = " ".join(green_badge(s) for s in match.get("matched_skills", [])) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(16,185,129,0.04);border:1px solid rgba(16,185,129,0.12)">' | |
| f'<div style="font-size:12px;font-weight:600;color:#34d399;margin-bottom:12px">✓ EŞLEŞEn BECERİLER</div>' | |
| f'{badges}</div>', unsafe_allow_html=True | |
| ) | |
| with c_miss: | |
| badges = " ".join(red_badge(s) for s in match.get("missing_skills", [])) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(239,68,68,0.04);border:1px solid rgba(239,68,68,0.12)">' | |
| f'<div style="font-size:12px;font-weight:600;color:#f87171;margin-bottom:12px">✗ EKSİK BECERİLER</div>' | |
| f'{badges}</div>', unsafe_allow_html=True | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| hp_items = "".join( | |
| f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#60a5fa">◦</span> {h}</div>' | |
| for h in match.get("highlight_points", []) | |
| ) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(59,130,246,0.04);border:1px solid rgba(59,130,246,0.12);margin-bottom:16px">' | |
| f'<div style="font-size:12px;font-weight:600;color:#60a5fa;margin-bottom:12px">◎ ÖNE ÇIKARILACAK NOKTALAR</div>' | |
| f'{hp_items}</div>', unsafe_allow_html=True | |
| ) | |
| tip_items = "".join( | |
| f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#fbbf24">→</span> {t}</div>' | |
| for t in match.get("improvement_tips", []) | |
| ) | |
| st.markdown( | |
| f'<div style="padding:20px;border-radius:14px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.12)">' | |
| f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:12px">⟳ İYİLEŞTİRME ÖNERİLERİ</div>' | |
| f'{tip_items}</div>', unsafe_allow_html=True | |
| ) | |
| # ── Cover Letter Tab ─────────────────────────────────────────────────────── | |
| with tab_letter: | |
| ksp = letter.get("key_selling_points", []) | |
| if ksp: | |
| badges = " ".join(amber_badge(p) for p in ksp) | |
| st.markdown( | |
| f'<div style="padding:16px 20px;border-radius:12px;margin-bottom:20px;' | |
| f'background:rgba(245,158,11,0.06);border:1px solid rgba(245,158,11,0.15)">' | |
| f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:10px">✦ ÖNEMLİ SATIŞ NOKTALARI</div>' | |
| f'{badges}</div>', unsafe_allow_html=True | |
| ) | |
| if letter.get("subject_line"): | |
| st.markdown( | |
| f'<div style="padding:12px 16px;border-radius:10px;margin-bottom:16px;' | |
| f'background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)">' | |
| f'<span style="font-size:11px;color:#64748b;margin-right:8px">KONU:</span>' | |
| f'<span style="font-size:13px;color:#e2e8f0">{letter["subject_line"]}</span>' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| cover = letter.get("cover_letter", "") | |
| st.markdown( | |
| f'<div style="padding:28px;border-radius:14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07)">' | |
| f'<pre style="font-family:Georgia,serif;font-size:14px;line-height:1.9;color:#cbd5e1;white-space:pre-wrap;margin:0">{cover}</pre>' | |
| f'</div>', unsafe_allow_html=True | |
| ) | |
| if st.button("⧉ Kopyala", key="copy_letter"): | |
| st.code(cover, language=None) | |
| # ── Tool Log Tab ─────────────────────────────────────────────────────────── | |
| with tab_log: | |
| for entry in st.session_state.tool_log: | |
| with st.expander(f"🔧 TOOL CALL → {entry['name']}"): | |
| st.json(entry["result"]) | |
| # New analysis button | |
| st.markdown("<br><br>", unsafe_allow_html=True) | |
| _, center, _ = st.columns([2, 1, 2]) | |
| with center: | |
| if st.button("← Yeni Analiz", use_container_width=True): | |
| st.session_state.stage = "input" | |
| st.session_state.results = {} | |
| st.session_state.tool_log = [] | |
| st.rerun() | |