CananD's picture
Update app.py
f48ab62 verified
"""
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 &amp; GEMINI · MULTI-STEP TOOL USE</div>
</div>
</div>
<div>
""" +
green_badge("Function Calling") + "&nbsp;" +
blue_badge("Context Management") + "&nbsp;" +
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()