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