"""
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("""
""", 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'{text} '
)
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"""
{score}
{label}
{score}/100
"""
# ─── 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"\n{cv_text}\n \n\n"
f"\n{job_desc}\n \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"\n{cv_text}\n \n\n"
f"\n{job_desc}\n \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("""
CV
CV + Portfolio Analyzer
POWERED BY CLAUDE & GEMINI · MULTI-STEP TOOL USE
""" +
green_badge("Function Calling") + " " +
blue_badge("Context Management") + " " +
amber_badge("Multi-step Reasoning") +
"""
""", 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(
'🤖 claude-opus-4-5
',
unsafe_allow_html=True
)
else:
st.markdown(
'🤖 gemini-2.0-flash
',
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('CV / Özgeçmiş
', 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('İş İlanı
', 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(" ", 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''
f'
{step["icon"]}
'
f'
{step["label"]}
'
f'
{step["desc"]}
'
f'
', unsafe_allow_html=True
)
step_placeholders.append(ph)
st.markdown(" ", 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'✓
'
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'{step["icon"]}
'
bg = "rgba(245,158,11,0.08)"
border = "#f59e0b"
label_color = "#f59e0b"
else:
icon_html = f'{step["icon"]}
'
bg = "rgba(255,255,255,0.03)"
border = "rgba(255,255,255,0.06)"
label_color = "#475569"
step_placeholders[i].markdown(
f''
f'{icon_html}'
f'
{step["label"]}
'
f'
{step["desc"]}
'
f'
', unsafe_allow_html=True
)
def render_log(entries):
if not entries:
log_ph.markdown(
''
'Tool call log bekleniyor...'
'
', unsafe_allow_html=True
)
return
html = ''
html += '
TOOL CALL LOG
'
for e in entries:
preview = json.dumps(e["result"])[:200] + ("..." if len(json.dumps(e["result"])) > 200 else "")
html += f'''
TOOL CALL → {e["name"]}
✓ Tamamlandı
{preview}
'''
html += '
'
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''
f'▸ {event["msg"]}
', 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(
''
'✓ Analiz tamamlandı!
', 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''
f'
Bulunan Bölümler
'
f'{badges_html}'
f'
', unsafe_allow_html=True
)
st.markdown(" ", unsafe_allow_html=True)
# Strengths & Gaps
c_str, c_gap = st.columns(2)
with c_str:
items = "".join(
f'→ {s}
'
for s in cv.get("key_strengths", [])
)
st.markdown(
f''
f'
✦ Güçlü Yanlar
'
f'{items}
', unsafe_allow_html=True
)
with c_gap:
items = "".join(
f'→ {g}
'
for g in cv.get("critical_gaps", [])
)
st.markdown(
f''
f'
⚠ Kritik Eksikler
'
f'{items}
', unsafe_allow_html=True
)
# Missing sections
missing = cv.get("missing_sections", [])
if missing:
st.markdown(" ", unsafe_allow_html=True)
badges_html = " ".join(amber_badge(s) for s in missing)
st.markdown(
f''
f'
EKSİK BÖLÜMLER
'
f'{badges_html}
', unsafe_allow_html=True
)
# Recommendations
st.markdown(" ", unsafe_allow_html=True)
recs_html = "".join(
f''
f'{i+1} '
f'{r}
'
for i, r in enumerate(cv.get("recommendations", []))
)
st.markdown(
f''
f'
▣ Öneriler
'
f'{recs_html}
', unsafe_allow_html=True
)
# ── Match Tab ──────────────────────────────────────────────────────────────
with tab_match:
score = int(match.get("match_score", 0))
st.markdown(
f''
f'
{score}% Uyum
'
f'
{match.get("experience_match","")}
'
f'
', 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''
f'
✓ EŞLEŞEn BECERİLER
'
f'{badges}
', unsafe_allow_html=True
)
with c_miss:
badges = " ".join(red_badge(s) for s in match.get("missing_skills", []))
st.markdown(
f''
f'
✗ EKSİK BECERİLER
'
f'{badges}
', unsafe_allow_html=True
)
st.markdown(" ", unsafe_allow_html=True)
hp_items = "".join(
f'◦ {h}
'
for h in match.get("highlight_points", [])
)
st.markdown(
f''
f'
◎ ÖNE ÇIKARILACAK NOKTALAR
'
f'{hp_items}
', unsafe_allow_html=True
)
tip_items = "".join(
f'→ {t}
'
for t in match.get("improvement_tips", [])
)
st.markdown(
f''
f'
⟳ İYİLEŞTİRME ÖNERİLERİ
'
f'{tip_items}
', 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''
f'
✦ ÖNEMLİ SATIŞ NOKTALARI
'
f'{badges}
', unsafe_allow_html=True
)
if letter.get("subject_line"):
st.markdown(
f''
f'KONU: '
f'{letter["subject_line"]} '
f'
', unsafe_allow_html=True
)
cover = letter.get("cover_letter", "")
st.markdown(
f'', 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(" ", 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()