CananD commited on
Commit
2efd537
·
verified ·
1 Parent(s): a885ca3

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +904 -0
app.py ADDED
@@ -0,0 +1,904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CV + Portfolio Analyzer — Python/Streamlit
3
+ Powered by Claude & Gemini · Multi-step Tool Use
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import streamlit as st
9
+ import anthropic
10
+ import google.generativeai as genai
11
+ from google.generativeai.types import FunctionDeclaration, Tool as GeminiTool
12
+
13
+ # ─── Page Config ───────────────────────────────────────────────────────────────
14
+ st.set_page_config(
15
+ page_title="CV + Portfolio Analyzer",
16
+ page_icon="📄",
17
+ layout="wide",
18
+ )
19
+
20
+ # ─── Custom CSS ────────────────────────────────────────────────────────────────
21
+ st.markdown("""
22
+ <style>
23
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
24
+
25
+ html, body, [data-testid="stAppViewContainer"] {
26
+ background: #0a0f1a !important;
27
+ color: #e2e8f0 !important;
28
+ font-family: 'Inter', -apple-system, sans-serif !important;
29
+ }
30
+ [data-testid="stHeader"] { background: transparent !important; }
31
+
32
+ /* Hide streamlit default menu */
33
+ #MainMenu, footer, header { visibility: hidden; }
34
+
35
+ /* Sidebar */
36
+ [data-testid="stSidebar"] { display: none !important; }
37
+
38
+ /* Text areas */
39
+ textarea {
40
+ background: #ffffff !important;
41
+ border: 1px solid rgba(255,255,255,0.08) !important;
42
+ color: #000000 !important;
43
+ border-radius: 12px !important;
44
+ font-family: 'SF Mono', 'Fira Code', monospace !important;
45
+ font-size: 13px !important;
46
+ }
47
+
48
+ /* Text inputs */
49
+ input[type="text"], input[type="password"] {
50
+ background: rgba(255,255,255,0.04) !important;
51
+ border: 1px solid rgba(255,255,255,0.08) !important;
52
+ color: #e2e8f0 !important;
53
+ border-radius: 8px !important;
54
+ }
55
+
56
+ /* Buttons */
57
+ .stButton > button {
58
+ background: linear-gradient(135deg, #f59e0b, #ef4444) !important;
59
+ color: white !important;
60
+ border: none !important;
61
+ border-radius: 12px !important;
62
+ font-weight: 600 !important;
63
+ padding: 12px 40px !important;
64
+ font-size: 15px !important;
65
+ transition: transform 0.2s !important;
66
+ box-shadow: 0 4px 24px rgba(245,158,11,0.3) !important;
67
+ }
68
+ .stButton > button:hover { transform: translateY(-2px) !important; }
69
+
70
+ /* Tabs */
71
+ [data-testid="stTabs"] button {
72
+ background: transparent !important;
73
+ color: #64748b !important;
74
+ border-radius: 9px !important;
75
+ font-size: 13px !important;
76
+ }
77
+ [data-testid="stTabs"] button[aria-selected="true"] {
78
+ background: rgba(245,158,11,0.15) !important;
79
+ color: #f59e0b !important;
80
+ border-bottom: 2px solid #f59e0b !important;
81
+ }
82
+
83
+ /* Metrics */
84
+ [data-testid="stMetric"] {
85
+ background: rgba(255,255,255,0.04) !important;
86
+ border: 1px solid rgba(255,255,255,0.08) !important;
87
+ border-radius: 14px !important;
88
+ padding: 16px !important;
89
+ }
90
+ [data-testid="stMetricValue"] { color: white !important; font-size: 28px !important; }
91
+ [data-testid="stMetricLabel"] { color: #64748b !important; font-size: 12px !important; }
92
+
93
+ /* Progress bar */
94
+ .stProgress > div > div {
95
+ background: linear-gradient(90deg, #f59e0b, #ef4444) !important;
96
+ border-radius: 4px !important;
97
+ }
98
+
99
+ /* Expanders */
100
+ [data-testid="stExpander"] {
101
+ background: rgba(255,255,255,0.03) !important;
102
+ border: 1px solid rgba(255,255,255,0.07) !important;
103
+ border-radius: 12px !important;
104
+ }
105
+
106
+ /* Divider */
107
+ hr { border-color: rgba(255,255,255,0.06) !important; }
108
+
109
+ /* Labels */
110
+ label { color: #94a3b8 !important; font-size: 12px !important; font-weight: 600 !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; }
111
+ </style>
112
+ """, unsafe_allow_html=True)
113
+
114
+ # ─── Tool Definitions ─────────────────────────────────────────────────────────
115
+ TOOLS = [
116
+ {
117
+ "name": "analyze_cv_sections",
118
+ "description": "Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.",
119
+ "input_schema": {
120
+ "type": "object",
121
+ "properties": {
122
+ "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"},
123
+ "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"},
124
+ "section_scores": {
125
+ "type": "object",
126
+ "description": "Quality score per section (0-10)",
127
+ "additionalProperties": {"type": "number"}
128
+ },
129
+ "overall_score": {"type": "number", "description": "CV strength score (0-100)"},
130
+ "key_strengths": {"type": "array", "items": {"type": "string"}},
131
+ "critical_gaps": {"type": "array", "items": {"type": "string"}},
132
+ "recommendations": {"type": "array", "items": {"type": "string"}}
133
+ },
134
+ "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"]
135
+ }
136
+ },
137
+ {
138
+ "name": "calculate_job_match",
139
+ "description": "Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.",
140
+ "input_schema": {
141
+ "type": "object",
142
+ "properties": {
143
+ "match_score": {"type": "number", "description": "Match percentage 0-100"},
144
+ "matched_skills": {"type": "array", "items": {"type": "string"}},
145
+ "missing_skills": {"type": "array", "items": {"type": "string"}},
146
+ "experience_match": {"type": "string"},
147
+ "highlight_points": {"type": "array", "items": {"type": "string"}, "description": "Strong alignment points to emphasize"},
148
+ "improvement_tips": {"type": "array", "items": {"type": "string"}}
149
+ },
150
+ "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"]
151
+ }
152
+ },
153
+ {
154
+ "name": "write_cover_letter",
155
+ "description": "Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.",
156
+ "input_schema": {
157
+ "type": "object",
158
+ "properties": {
159
+ "subject_line": {"type": "string"},
160
+ "cover_letter": {"type": "string", "description": "Full formatted cover letter"},
161
+ "key_selling_points": {"type": "array", "items": {"type": "string"}}
162
+ },
163
+ "required": ["cover_letter", "key_selling_points"]
164
+ }
165
+ }
166
+ ]
167
+
168
+ # ─── Gemini Tool Definitions (function declarations) ──────────────────────────
169
+ GEMINI_TOOLS = [GeminiTool(function_declarations=[
170
+ FunctionDeclaration(
171
+ name="analyze_cv_sections",
172
+ description="Analyze the CV structure, identify all sections present, rate each section quality, detect critical gaps, and provide improvement recommendations.",
173
+ parameters={
174
+ "type": "object",
175
+ "properties": {
176
+ "sections_found": {"type": "array", "items": {"type": "string"}, "description": "Sections detected in the CV"},
177
+ "missing_sections": {"type": "array", "items": {"type": "string"}, "description": "Important missing sections"},
178
+ "overall_score": {"type": "number", "description": "CV strength score (0-100)"},
179
+ "key_strengths": {"type": "array", "items": {"type": "string"}},
180
+ "critical_gaps": {"type": "array", "items": {"type": "string"}},
181
+ "recommendations": {"type": "array", "items": {"type": "string"}},
182
+ },
183
+ "required": ["sections_found", "missing_sections", "overall_score", "key_strengths", "critical_gaps", "recommendations"],
184
+ },
185
+ ),
186
+ FunctionDeclaration(
187
+ name="calculate_job_match",
188
+ description="Compare CV against the job description. Identify matched/missing skills, calculate a match percentage, and give actionable tips.",
189
+ parameters={
190
+ "type": "object",
191
+ "properties": {
192
+ "match_score": {"type": "number", "description": "Match percentage 0-100"},
193
+ "matched_skills": {"type": "array", "items": {"type": "string"}},
194
+ "missing_skills": {"type": "array", "items": {"type": "string"}},
195
+ "experience_match": {"type": "string"},
196
+ "highlight_points": {"type": "array", "items": {"type": "string"}},
197
+ "improvement_tips": {"type": "array", "items": {"type": "string"}},
198
+ },
199
+ "required": ["match_score", "matched_skills", "missing_skills", "highlight_points", "improvement_tips"],
200
+ },
201
+ ),
202
+ FunctionDeclaration(
203
+ name="write_cover_letter",
204
+ description="Generate a personalized, compelling cover letter tailored to the job description using insights from the CV analysis.",
205
+ parameters={
206
+ "type": "object",
207
+ "properties": {
208
+ "subject_line": {"type": "string"},
209
+ "cover_letter": {"type": "string", "description": "Full formatted cover letter"},
210
+ "key_selling_points": {"type": "array", "items": {"type": "string"}},
211
+ },
212
+ "required": ["cover_letter", "key_selling_points"],
213
+ },
214
+ ),
215
+ ])]
216
+
217
+ STEPS = [
218
+ {"id": "analyze_cv_sections", "label": "CV Analizi", "icon": "▣", "desc": "Bölümler, güçlü yanlar ve eksikler tespit ediliyor"},
219
+ {"id": "calculate_job_match", "label": "İş Eşleştirme", "icon": "◎", "desc": "Pozisyona uyum oranı hesaplanıyor"},
220
+ {"id": "write_cover_letter", "label": "Ön Yazı", "icon": "✦", "desc": "Kişiselleştirilmiş cover letter oluşturuluyor"},
221
+ ]
222
+
223
+ # ─── Sample Data ───────────────────────────────────────────────────────────────
224
+ SAMPLE_CV = """John Doe
225
+ john@example.com | +1 (555) 123-4567 | linkedin.com/in/johndoe | github.com/johndoe
226
+
227
+ SUMMARY
228
+ Full-stack developer with 4 years of experience building scalable web applications. Passionate about clean code, performance optimization, and developer experience.
229
+
230
+ EXPERIENCE
231
+ Senior Frontend Developer — TechCorp (2022–Present)
232
+ - Built React/TypeScript dashboard used by 50k daily users
233
+ - Reduced page load time by 40% through code splitting and lazy loading
234
+ - Mentored 3 junior developers
235
+
236
+ Frontend Developer — StartupXYZ (2020–2022)
237
+ - Developed e-commerce platform with Next.js and Node.js
238
+ - Integrated Stripe payment system and REST APIs
239
+
240
+ EDUCATION
241
+ B.Sc. Computer Science — State University (2016–2020)
242
+
243
+ SKILLS
244
+ JavaScript, TypeScript, React, Next.js, Node.js, PostgreSQL, Docker, AWS"""
245
+
246
+ SAMPLE_JD = """Senior Full-Stack Engineer — FinTech Startup
247
+
248
+ We're looking for an experienced full-stack engineer to join our growing team.
249
+
250
+ Requirements:
251
+ - 5+ years of full-stack development experience
252
+ - Strong proficiency in React, TypeScript, and Node.js
253
+ - Experience with microservices architecture
254
+ - Knowledge of financial systems or payment processing (Stripe, Plaid)
255
+ - GraphQL API design and implementation
256
+ - AWS/GCP cloud infrastructure experience
257
+ - Leadership and mentoring skills
258
+ - Strong communication and teamwork"""
259
+
260
+
261
+ # ─── Helper: colored badge ─────────────────────────────────────────────────────
262
+ def badge(text: str, color: str = "#f59e0b", bg: str = "rgba(245,158,11,0.12)") -> str:
263
+ return (
264
+ f'<span style="display:inline-block;padding:3px 10px;border-radius:20px;'
265
+ f'font-size:12px;background:{bg};color:{color};'
266
+ f'border:1px solid {color}33;white-space:nowrap;margin:2px">{text}</span>'
267
+ )
268
+
269
+
270
+ def green_badge(t): return badge(t, "#34d399", "rgba(16,185,129,0.12)")
271
+ def red_badge(t): return badge(t, "#f87171", "rgba(239,68,68,0.12)")
272
+ def amber_badge(t): return badge(t, "#fbbf24", "rgba(245,158,11,0.12)")
273
+ def blue_badge(t): return badge(t, "#60a5fa", "rgba(59,130,246,0.12)")
274
+
275
+
276
+ def score_circle(score: int, label: str, color: str = "#f59e0b"):
277
+ pct = max(0, min(100, score))
278
+ return f"""
279
+ <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;">
280
+ <svg width="80" height="80" style="transform:rotate(-90deg)">
281
+ <circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="6"/>
282
+ <circle cx="40" cy="40" r="34" fill="none" stroke="{color}"
283
+ stroke-width="6" stroke-linecap="round"
284
+ stroke-dasharray="{pct/100*213.6:.1f} {213.6 - pct/100*213.6:.1f}"
285
+ style="transition:stroke-dasharray 1s ease"/>
286
+ <text x="40" y="40" text-anchor="middle" dominant-baseline="central"
287
+ fill="white" font-size="15" font-weight="600"
288
+ style="transform:rotate(90deg) translate(0px,-80px)">{score}</text>
289
+ </svg>
290
+ <div style="color:#64748b;font-size:12px;margin-top:6px">{label}</div>
291
+ <div style="color:white;font-size:20px;font-weight:700">{score}/100</div>
292
+ </div>"""
293
+
294
+
295
+ # ─── Core: run analysis ─────────────────────────────────────────────────────────
296
+ def run_analysis(cv_text: str, job_desc: str, api_key: str):
297
+ """Generator that yields status dicts as Claude streams tool results."""
298
+ client = anthropic.Anthropic(api_key=api_key)
299
+
300
+ messages = [
301
+ {
302
+ "role": "user",
303
+ "content": (
304
+ "You are an expert career coach and CV analyzer. "
305
+ "Analyze the following CV and job description thoroughly using all three available tools in sequence.\n\n"
306
+ f"<cv>\n{cv_text}\n</cv>\n\n"
307
+ f"<job_description>\n{job_desc}\n</job_description>\n\n"
308
+ "Please:\n"
309
+ "1. First call analyze_cv_sections to evaluate the CV quality\n"
310
+ "2. Then call calculate_job_match to assess alignment with the job\n"
311
+ "3. Finally call write_cover_letter to create a compelling application letter\n\n"
312
+ "Use all three tools."
313
+ )
314
+ }
315
+ ]
316
+
317
+ results = {"cv": None, "match": None, "letter": None}
318
+ tool_log = []
319
+
320
+ continue_loop = True
321
+ while continue_loop:
322
+ yield {"type": "status", "msg": "Claude API'ye istek gönderiliyor..."}
323
+
324
+ response = client.messages.create(
325
+ model="claude-opus-4-5",
326
+ max_tokens=4000,
327
+ tools=TOOLS,
328
+ messages=messages,
329
+ )
330
+
331
+ messages.append({"role": "assistant", "content": response.content})
332
+
333
+ tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
334
+
335
+ if not tool_use_blocks:
336
+ continue_loop = False
337
+ break
338
+
339
+ tool_results_msg = []
340
+
341
+ for block in tool_use_blocks:
342
+ tool_name = block.name
343
+ tool_input = block.input
344
+
345
+ step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1)
346
+ step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name
347
+
348
+ yield {"type": "step", "step_idx": step_idx, "label": step_label}
349
+ yield {"type": "status", "msg": f"{step_label} çalışıyor..."}
350
+
351
+ # Store results
352
+ if tool_name == "analyze_cv_sections":
353
+ results["cv"] = tool_input
354
+ elif tool_name == "calculate_job_match":
355
+ results["match"] = tool_input
356
+ elif tool_name == "write_cover_letter":
357
+ results["letter"] = tool_input
358
+
359
+ tool_log.append({"name": tool_name, "result": tool_input})
360
+ yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log}
361
+
362
+ tool_results_msg.append({
363
+ "type": "tool_result",
364
+ "tool_use_id": block.id,
365
+ "content": json.dumps(tool_input),
366
+ })
367
+
368
+ messages.append({"role": "user", "content": tool_results_msg})
369
+
370
+ if response.stop_reason == "end_turn":
371
+ continue_loop = False
372
+
373
+ yield {"type": "done", "results": results, "log": tool_log}
374
+
375
+
376
+ # ─── Core: run analysis with Gemini ────────────────────────────────────────────
377
+ def run_analysis_gemini(cv_text: str, job_desc: str, api_key: str):
378
+ """Generator that yields status dicts as Gemini streams tool results."""
379
+ genai.configure(api_key=api_key)
380
+ model = genai.GenerativeModel(
381
+ model_name="gemini-2.0-flash",
382
+ tools=GEMINI_TOOLS,
383
+ system_instruction=(
384
+ "You are an expert career coach and CV analyzer. "
385
+ "Use all three available tools in sequence: "
386
+ "first analyze_cv_sections, then calculate_job_match, then write_cover_letter."
387
+ ),
388
+ )
389
+
390
+ user_prompt = (
391
+ f"Analyze this CV and job description using all three tools in order.\n\n"
392
+ f"<cv>\n{cv_text}\n</cv>\n\n"
393
+ f"<job_description>\n{job_desc}\n</job_description>\n\n"
394
+ "1. Call analyze_cv_sections\n"
395
+ "2. Call calculate_job_match\n"
396
+ "3. Call write_cover_letter\n"
397
+ "Use all three tools."
398
+ )
399
+
400
+ results = {"cv": None, "match": None, "letter": None}
401
+ tool_log = []
402
+ history = []
403
+
404
+ # Ordered list of tools to call
405
+ tools_to_call = ["analyze_cv_sections", "calculate_job_match", "write_cover_letter"]
406
+ current_prompt = user_prompt
407
+
408
+ for expected_tool in tools_to_call:
409
+ yield {"type": "status", "msg": "Gemini API'ye istek gönderiliyor..."}
410
+
411
+ chat = model.start_chat(history=history)
412
+ response = chat.send_message(current_prompt)
413
+
414
+ # Find function call in response
415
+ fc = None
416
+ for part in response.parts:
417
+ if hasattr(part, "function_call") and part.function_call.name:
418
+ fc = part.function_call
419
+ break
420
+
421
+ if fc is None:
422
+ # Try to extract JSON from text as fallback
423
+ break
424
+
425
+ tool_name = fc.name
426
+ # Convert Gemini MapComposite to plain dict
427
+ tool_input = dict(fc.args)
428
+ # Recursively convert nested MapComposite / ListValue objects
429
+ tool_input = json.loads(json.dumps(tool_input, default=str))
430
+ # De-nest arrays stored as {"values": [...]}
431
+ for k, v in tool_input.items():
432
+ if isinstance(v, dict) and list(v.keys()) == ["values"]:
433
+ tool_input[k] = v["values"]
434
+
435
+ step_idx = next((i for i, s in enumerate(STEPS) if s["id"] == tool_name), -1)
436
+ step_label = STEPS[step_idx]["label"] if step_idx >= 0 else tool_name
437
+
438
+ yield {"type": "step", "step_idx": step_idx, "label": step_label}
439
+ yield {"type": "status", "msg": f"{step_label} çalışıyor..."}
440
+
441
+ if tool_name == "analyze_cv_sections":
442
+ results["cv"] = tool_input
443
+ elif tool_name == "calculate_job_match":
444
+ results["match"] = tool_input
445
+ elif tool_name == "write_cover_letter":
446
+ results["letter"] = tool_input
447
+
448
+ tool_log.append({"name": tool_name, "result": tool_input})
449
+ yield {"type": "tool_done", "name": tool_name, "result": tool_input, "results": results, "log": tool_log}
450
+
451
+ # Build history for next turn: assistant called the function, we return the result
452
+ history = chat.history + [
453
+ {
454
+ "role": "user",
455
+ "parts": [{"function_response": {"name": tool_name, "response": {"result": str(tool_input)}}}],
456
+ }
457
+ ]
458
+ current_prompt = (
459
+ f"Good. Now call the next tool."
460
+ )
461
+
462
+ yield {"type": "done", "results": results, "log": tool_log}
463
+
464
+
465
+ # ─── Session State Init ─────────────────────────────────────────────────────────
466
+ for key, default in [
467
+ ("stage", "input"), # input | loading | results
468
+ ("results", {}),
469
+ ("tool_log", []),
470
+ ("cv_text", SAMPLE_CV),
471
+ ("job_desc", SAMPLE_JD),
472
+ ("provider", "Claude"),
473
+ ]:
474
+ if key not in st.session_state:
475
+ st.session_state[key] = default
476
+
477
+
478
+ # ─── Header ────────────────────────────────────────────────────────────────────
479
+ st.markdown("""
480
+ <div style="border-bottom:1px solid rgba(255,255,255,0.06);padding:18px 0 16px 0;margin-bottom:32px;">
481
+ <div style="display:flex;align-items:center;justify-content:space-between;">
482
+ <div style="display:flex;align-items:center;gap:12px;">
483
+ <div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#f59e0b,#ef4444);
484
+ display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:white;">CV</div>
485
+ <div>
486
+ <div style="font-size:16px;font-weight:700;color:white;">CV + Portfolio Analyzer</div>
487
+ <div style="font-size:11px;color:#475569;letter-spacing:0.06em;">POWERED BY CLAUDE &amp; GEMINI · MULTI-STEP TOOL USE</div>
488
+ </div>
489
+ </div>
490
+ <div>
491
+ """ +
492
+ green_badge("Function Calling") + "&nbsp;" +
493
+ blue_badge("Context Management") + "&nbsp;" +
494
+ amber_badge("Multi-step Reasoning") +
495
+ """
496
+ </div>
497
+ </div>
498
+ </div>
499
+ """, unsafe_allow_html=True)
500
+
501
+
502
+ # ─── Settings Expander (HF Spaces uyumlu) ────────────────────────────────
503
+ with st.expander("⚙️ Ayarlar — API Anahtarı & Model Seçimi", expanded=not st.session_state.get("active_key")):
504
+ col_radio, col_key, col_info = st.columns([1, 2, 1])
505
+
506
+ with col_radio:
507
+ provider = st.radio(
508
+ "AI Sağlayıcı",
509
+ options=["Claude", "Gemini"],
510
+ index=0 if st.session_state.provider == "Claude" else 1,
511
+ horizontal=False,
512
+ help="Hangi AI modeli kullanılsın?"
513
+ )
514
+ st.session_state.provider = provider
515
+
516
+ with col_key:
517
+ if provider == "Claude":
518
+ api_key = st.text_input(
519
+ "Anthropic API Anahtarı",
520
+ value=os.environ.get("ANTHROPIC_API_KEY", ""),
521
+ type="password",
522
+ placeholder="sk-ant-...",
523
+ help="https://console.anthropic.com"
524
+ )
525
+ gemini_key = ""
526
+ else:
527
+ api_key = ""
528
+ gemini_key = st.text_input(
529
+ "Google Gemini API Anahtarı",
530
+ value=os.environ.get("GOOGLE_API_KEY", ""),
531
+ type="password",
532
+ placeholder="AIza...",
533
+ help="https://aistudio.google.com/app/apikey"
534
+ )
535
+
536
+ with col_info:
537
+ if provider == "Claude":
538
+ st.markdown(
539
+ '<div style="padding:8px 12px;border-radius:8px;margin-top:24px;'
540
+ 'background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);'
541
+ 'font-size:12px;color:#f59e0b">🤖 claude-opus-4-5</div>',
542
+ unsafe_allow_html=True
543
+ )
544
+ else:
545
+ st.markdown(
546
+ '<div style="padding:8px 12px;border-radius:8px;margin-top:24px;'
547
+ 'background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.2);'
548
+ 'font-size:12px;color:#60a5fa">🤖 gemini-2.0-flash</div>',
549
+ unsafe_allow_html=True
550
+ )
551
+
552
+
553
+ # ════════════════════════════════════════════════════════════════════════════════
554
+ # STAGE: INPUT
555
+ # ════════════════════════════════════════════════════════════════════════════════
556
+ if st.session_state.stage == "input":
557
+ col1, col2 = st.columns(2)
558
+
559
+ with col1:
560
+ hdr1, btn1 = st.columns([3, 1])
561
+ with hdr1:
562
+ 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)
563
+ with btn1:
564
+ if st.button("Örnek", key="sample_cv", help="Örnek CV yükle"):
565
+ st.session_state.cv_text = SAMPLE_CV
566
+ st.rerun()
567
+ cv_input = st.text_area("cv_area", value=st.session_state.cv_text, height=360,
568
+ label_visibility="collapsed", key="cv_input_area")
569
+
570
+ with col2:
571
+ hdr2, btn2 = st.columns([3, 1])
572
+ with hdr2:
573
+ 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)
574
+ with btn2:
575
+ if st.button("Örnek", key="sample_jd", help="Örnek iş ilanı yükle"):
576
+ st.session_state.job_desc = SAMPLE_JD
577
+ st.rerun()
578
+ jd_input = st.text_area("jd_area", value=st.session_state.job_desc, height=360,
579
+ label_visibility="collapsed", key="jd_input_area")
580
+
581
+ st.markdown("<br>", unsafe_allow_html=True)
582
+ _, center, _ = st.columns([2, 1, 2])
583
+ with center:
584
+ start = st.button("✦ Analizi Başlat", use_container_width=True)
585
+
586
+ if start:
587
+ active_key = api_key if st.session_state.provider == "Claude" else gemini_key
588
+ if not active_key:
589
+ pname = "Anthropic" if st.session_state.provider == "Claude" else "Google Gemini"
590
+ st.error(f"⚠️ Lütfen sol panelden {pname} API anahtarınızı girin.")
591
+ elif not cv_input.strip() or not jd_input.strip():
592
+ st.error("Lütfen CV ve iş ilanı alanlarını doldurun.")
593
+ else:
594
+ st.session_state.cv_text = cv_input
595
+ st.session_state.job_desc = jd_input
596
+ st.session_state.active_key = active_key
597
+ st.session_state.stage = "loading"
598
+ st.session_state.results = {}
599
+ st.session_state.tool_log = []
600
+ st.rerun()
601
+
602
+
603
+ # ════════════════════════════════════════════════════════════════════════════════
604
+ # STAGE: LOADING
605
+ # ════════════════════════════════════════════════════════════════════════════════
606
+ elif st.session_state.stage == "loading":
607
+
608
+ # Step progress header
609
+ step_cols = st.columns(len(STEPS))
610
+ step_placeholders = []
611
+ for i, step in enumerate(STEPS):
612
+ with step_cols[i]:
613
+ ph = st.empty()
614
+ ph.markdown(
615
+ f'<div style="text-align:center;padding:12px;border-radius:12px;'
616
+ f'background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)">'
617
+ f'<div style="font-size:22px">{step["icon"]}</div>'
618
+ f'<div style="font-size:13px;font-weight:600;color:#94a3b8;margin-top:6px">{step["label"]}</div>'
619
+ f'<div style="font-size:11px;color:#475569;margin-top:4px">{step["desc"]}</div>'
620
+ f'</div>', unsafe_allow_html=True
621
+ )
622
+ step_placeholders.append(ph)
623
+
624
+ st.markdown("<br>", unsafe_allow_html=True)
625
+ status_ph = st.empty()
626
+ progress_ph = st.empty()
627
+ log_ph = st.empty()
628
+
629
+ active_step = 0
630
+ partial_results = {}
631
+ log_entries = []
632
+
633
+ def render_step(idx, done_steps):
634
+ for i, step in enumerate(STEPS):
635
+ if i < done_steps:
636
+ icon_html = f'<div style="font-size:18px;color:#34d399">✓</div>'
637
+ bg = "linear-gradient(135deg,rgba(16,185,129,0.15),rgba(16,185,129,0.05))"
638
+ border = "rgba(16,185,129,0.25)"
639
+ label_color = "#34d399"
640
+ elif i == done_steps:
641
+ icon_html = f'<div style="font-size:22px;animation:spin 1s linear infinite">{step["icon"]}</div>'
642
+ bg = "rgba(245,158,11,0.08)"
643
+ border = "#f59e0b"
644
+ label_color = "#f59e0b"
645
+ else:
646
+ icon_html = f'<div style="font-size:22px;color:#475569">{step["icon"]}</div>'
647
+ bg = "rgba(255,255,255,0.03)"
648
+ border = "rgba(255,255,255,0.06)"
649
+ label_color = "#475569"
650
+
651
+ step_placeholders[i].markdown(
652
+ f'<div style="text-align:center;padding:12px;border-radius:12px;'
653
+ f'background:{bg};border:2px solid {border};transition:all 0.4s">'
654
+ f'{icon_html}'
655
+ f'<div style="font-size:13px;font-weight:600;color:{label_color};margin-top:6px">{step["label"]}</div>'
656
+ f'<div style="font-size:11px;color:#475569;margin-top:4px">{step["desc"]}</div>'
657
+ f'</div>', unsafe_allow_html=True
658
+ )
659
+
660
+ def render_log(entries):
661
+ if not entries:
662
+ log_ph.markdown(
663
+ '<div style="font-family:monospace;font-size:12px;padding:16px;'
664
+ 'background:rgba(0,0,0,0.3);border-radius:12px;border:1px solid rgba(255,255,255,0.06);color:#475569">'
665
+ 'Tool call log bekleniyor...'
666
+ '</div>', unsafe_allow_html=True
667
+ )
668
+ return
669
+ 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)">'
670
+ html += '<div style="font-size:11px;color:#475569;margin-bottom:10px;letter-spacing:0.08em">TOOL CALL LOG</div>'
671
+ for e in entries:
672
+ preview = json.dumps(e["result"])[:200] + ("..." if len(json.dumps(e["result"])) > 200 else "")
673
+ html += f'''
674
+ <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)">
675
+ <div style="color:#f59e0b;font-size:11px;letter-spacing:0.05em;margin-bottom:6px">TOOL CALL → {e["name"]}</div>
676
+ <div style="color:#34d399;font-size:11px;margin-bottom:6px">✓ Tamamlandı</div>
677
+ <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>
678
+ </div>'''
679
+ html += '</div>'
680
+ log_ph.markdown(html, unsafe_allow_html=True)
681
+
682
+ render_step(0, 0)
683
+ render_log([])
684
+
685
+ try:
686
+ done_count = 0
687
+ _key = st.session_state.get("active_key", "")
688
+ _provider = st.session_state.get("provider", "Claude")
689
+ _fn = run_analysis if _provider == "Claude" else run_analysis_gemini
690
+ for event in _fn(st.session_state.cv_text, st.session_state.job_desc, _key):
691
+ if event["type"] == "status":
692
+ status_ph.markdown(
693
+ f'<div style="text-align:center;color:#94a3b8;font-family:monospace;font-size:13px;padding:8px">'
694
+ f'▸ {event["msg"]}</div>', unsafe_allow_html=True
695
+ )
696
+ progress_ph.progress(done_count / 3)
697
+
698
+ elif event["type"] == "step":
699
+ render_step(event["step_idx"], done_count)
700
+
701
+ elif event["type"] == "tool_done":
702
+ log_entries.append({"name": event["name"], "result": event["result"]})
703
+ done_count += 1
704
+ partial_results = event["results"]
705
+ render_step(done_count, done_count)
706
+ render_log(log_entries)
707
+ progress_ph.progress(done_count / 3)
708
+
709
+ elif event["type"] == "done":
710
+ st.session_state.results = event["results"]
711
+ st.session_state.tool_log = event["log"]
712
+ progress_ph.progress(1.0)
713
+ status_ph.markdown(
714
+ '<div style="text-align:center;color:#34d399;font-family:monospace;font-size:13px;padding:8px">'
715
+ '✓ Analiz tamamlandı!</div>', unsafe_allow_html=True
716
+ )
717
+ import time; time.sleep(0.5)
718
+ st.session_state.stage = "results"
719
+ st.rerun()
720
+
721
+ except Exception as e:
722
+ st.error(f"Hata: {e}")
723
+ if st.button("← Geri Dön"):
724
+ st.session_state.stage = "input"
725
+ st.rerun()
726
+
727
+
728
+ # ════════════════════════════════════════════════════════════════════════════════
729
+ # STAGE: RESULTS
730
+ # ════════════════════════════════════════════════════════════════════════════════
731
+ elif st.session_state.stage == "results":
732
+ results = st.session_state.results
733
+ cv = results.get("cv") or {}
734
+ match = results.get("match") or {}
735
+ letter = results.get("letter") or {}
736
+
737
+ tab_overview, tab_match, tab_letter, tab_log = st.tabs(
738
+ ["📊 Genel Bakış", "🎯 İş Eşleşmesi", "✉️ Cover Letter", "🔧 Tool Logs"]
739
+ )
740
+
741
+ # ── Overview Tab ───────────────────────────────────────────────────────────
742
+ with tab_overview:
743
+ # Score cards
744
+ m1, m2, m3 = st.columns(3)
745
+ with m1:
746
+ st.markdown(score_circle(int(cv.get("overall_score", 0)), "CV Puanı", "#f59e0b"), unsafe_allow_html=True)
747
+ with m2:
748
+ st.markdown(score_circle(int(match.get("match_score", 0)), "İş Uyumu", "#10b981"), unsafe_allow_html=True)
749
+ with m3:
750
+ sections = cv.get("sections_found", [])
751
+ badges_html = " ".join(blue_badge(s) for s in sections)
752
+ st.markdown(
753
+ 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">'
754
+ f'<div style="font-size:12px;color:#64748b;margin-bottom:10px">Bulunan Bölümler</div>'
755
+ f'{badges_html}'
756
+ f'</div>', unsafe_allow_html=True
757
+ )
758
+
759
+ st.markdown("<br>", unsafe_allow_html=True)
760
+
761
+ # Strengths & Gaps
762
+ c_str, c_gap = st.columns(2)
763
+ with c_str:
764
+ items = "".join(
765
+ f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#34d399">→</span> {s}</div>'
766
+ for s in cv.get("key_strengths", [])
767
+ )
768
+ st.markdown(
769
+ f'<div style="padding:20px;border-radius:14px;background:rgba(16,185,129,0.04);border:1px solid rgba(16,185,129,0.12)">'
770
+ f'<div style="font-size:13px;font-weight:600;color:#34d399;margin-bottom:12px">✦ Güçlü Yanlar</div>'
771
+ f'{items}</div>', unsafe_allow_html=True
772
+ )
773
+ with c_gap:
774
+ items = "".join(
775
+ f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#f87171">→</span> {g}</div>'
776
+ for g in cv.get("critical_gaps", [])
777
+ )
778
+ st.markdown(
779
+ f'<div style="padding:20px;border-radius:14px;background:rgba(239,68,68,0.04);border:1px solid rgba(239,68,68,0.12)">'
780
+ f'<div style="font-size:13px;font-weight:600;color:#f87171;margin-bottom:12px">⚠ Kritik Eksikler</div>'
781
+ f'{items}</div>', unsafe_allow_html=True
782
+ )
783
+
784
+ # Missing sections
785
+ missing = cv.get("missing_sections", [])
786
+ if missing:
787
+ st.markdown("<br>", unsafe_allow_html=True)
788
+ badges_html = " ".join(amber_badge(s) for s in missing)
789
+ st.markdown(
790
+ 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)">'
791
+ f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:10px">EKSİK BÖLÜMLER</div>'
792
+ f'{badges_html}</div>', unsafe_allow_html=True
793
+ )
794
+
795
+ # Recommendations
796
+ st.markdown("<br>", unsafe_allow_html=True)
797
+ recs_html = "".join(
798
+ f'<div style="display:flex;gap:12px;margin-bottom:10px;padding:10px 14px;border-radius:8px;background:rgba(255,255,255,0.03)">'
799
+ f'<span style="min-width:22px;height:22px;border-radius:50%;background:rgba(245,158,11,0.15);color:#f59e0b;'
800
+ f'display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700">{i+1}</span>'
801
+ f'<span style="font-size:13px;color:#94a3b8;line-height:1.6">{r}</span></div>'
802
+ for i, r in enumerate(cv.get("recommendations", []))
803
+ )
804
+ st.markdown(
805
+ f'<div style="padding:20px;border-radius:14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07)">'
806
+ f'<div style="font-size:13px;font-weight:600;color:#e2e8f0;margin-bottom:14px">▣ Öneriler</div>'
807
+ f'{recs_html}</div>', unsafe_allow_html=True
808
+ )
809
+
810
+ # ── Match Tab ──────────────────────────────────────────────────────────────
811
+ with tab_match:
812
+ score = int(match.get("match_score", 0))
813
+ st.markdown(
814
+ f'<div style="padding:24px;border-radius:14px;margin-bottom:20px;'
815
+ f'background:linear-gradient(135deg,rgba(16,185,129,0.08),rgba(59,130,246,0.08));'
816
+ f'border:1px solid rgba(16,185,129,0.15);">'
817
+ f'<div style="font-size:32px;font-weight:800;color:white">{score}% Uyum</div>'
818
+ f'<div style="font-size:13px;color:#64748b;margin-top:4px">{match.get("experience_match","")}</div>'
819
+ f'</div>', unsafe_allow_html=True
820
+ )
821
+ st.progress(score / 100)
822
+
823
+ c_ok, c_miss = st.columns(2)
824
+ with c_ok:
825
+ badges = " ".join(green_badge(s) for s in match.get("matched_skills", []))
826
+ st.markdown(
827
+ f'<div style="padding:20px;border-radius:14px;background:rgba(16,185,129,0.04);border:1px solid rgba(16,185,129,0.12)">'
828
+ f'<div style="font-size:12px;font-weight:600;color:#34d399;margin-bottom:12px">✓ EŞLEŞEn BECERİLER</div>'
829
+ f'{badges}</div>', unsafe_allow_html=True
830
+ )
831
+ with c_miss:
832
+ badges = " ".join(red_badge(s) for s in match.get("missing_skills", []))
833
+ st.markdown(
834
+ f'<div style="padding:20px;border-radius:14px;background:rgba(239,68,68,0.04);border:1px solid rgba(239,68,68,0.12)">'
835
+ f'<div style="font-size:12px;font-weight:600;color:#f87171;margin-bottom:12px">✗ EKSİK BECERİLER</div>'
836
+ f'{badges}</div>', unsafe_allow_html=True
837
+ )
838
+
839
+ st.markdown("<br>", unsafe_allow_html=True)
840
+ hp_items = "".join(
841
+ f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#60a5fa">◦</span> {h}</div>'
842
+ for h in match.get("highlight_points", [])
843
+ )
844
+ st.markdown(
845
+ 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">'
846
+ f'<div style="font-size:12px;font-weight:600;color:#60a5fa;margin-bottom:12px">◎ ÖNE ÇIKARILACAK NOKTALAR</div>'
847
+ f'{hp_items}</div>', unsafe_allow_html=True
848
+ )
849
+
850
+ tip_items = "".join(
851
+ f'<div style="display:flex;gap:8px;margin-bottom:8px;font-size:13px;color:#94a3b8"><span style="color:#fbbf24">→</span> {t}</div>'
852
+ for t in match.get("improvement_tips", [])
853
+ )
854
+ st.markdown(
855
+ f'<div style="padding:20px;border-radius:14px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.12)">'
856
+ f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:12px">⟳ İYİLEŞTİRME ÖNERİLERİ</div>'
857
+ f'{tip_items}</div>', unsafe_allow_html=True
858
+ )
859
+
860
+ # ── Cover Letter Tab ───────────────────────────────────────────────────────
861
+ with tab_letter:
862
+ ksp = letter.get("key_selling_points", [])
863
+ if ksp:
864
+ badges = " ".join(amber_badge(p) for p in ksp)
865
+ st.markdown(
866
+ f'<div style="padding:16px 20px;border-radius:12px;margin-bottom:20px;'
867
+ f'background:rgba(245,158,11,0.06);border:1px solid rgba(245,158,11,0.15)">'
868
+ f'<div style="font-size:12px;font-weight:600;color:#fbbf24;margin-bottom:10px">✦ ÖNEMLİ SATIŞ NOKTALARI</div>'
869
+ f'{badges}</div>', unsafe_allow_html=True
870
+ )
871
+
872
+ if letter.get("subject_line"):
873
+ st.markdown(
874
+ f'<div style="padding:12px 16px;border-radius:10px;margin-bottom:16px;'
875
+ f'background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)">'
876
+ f'<span style="font-size:11px;color:#64748b;margin-right:8px">KONU:</span>'
877
+ f'<span style="font-size:13px;color:#e2e8f0">{letter["subject_line"]}</span>'
878
+ f'</div>', unsafe_allow_html=True
879
+ )
880
+
881
+ cover = letter.get("cover_letter", "")
882
+ st.markdown(
883
+ f'<div style="padding:28px;border-radius:14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07)">'
884
+ f'<pre style="font-family:Georgia,serif;font-size:14px;line-height:1.9;color:#cbd5e1;white-space:pre-wrap;margin:0">{cover}</pre>'
885
+ f'</div>', unsafe_allow_html=True
886
+ )
887
+ if st.button("⧉ Kopyala", key="copy_letter"):
888
+ st.code(cover, language=None)
889
+
890
+ # ── Tool Log Tab ───────────────────────────────────────────────────────────
891
+ with tab_log:
892
+ for entry in st.session_state.tool_log:
893
+ with st.expander(f"🔧 TOOL CALL → {entry['name']}"):
894
+ st.json(entry["result"])
895
+
896
+ # New analysis button
897
+ st.markdown("<br><br>", unsafe_allow_html=True)
898
+ _, center, _ = st.columns([2, 1, 2])
899
+ with center:
900
+ if st.button("← Yeni Analiz", use_container_width=True):
901
+ st.session_state.stage = "input"
902
+ st.session_state.results = {}
903
+ st.session_state.tool_log = []
904
+ st.rerun()