Spaces:
Running
Running
Update src/app_job_copy_1.py
Browse files- src/app_job_copy_1.py +156 -205
src/app_job_copy_1.py
CHANGED
@@ -12,6 +12,7 @@ from langchain_core.output_parsers import StrOutputParser # Not directly used in
|
|
12 |
from langchain_core.prompts import PromptTemplate # Not directly used in provided snippet
|
13 |
import gspread
|
14 |
import tempfile
|
|
|
15 |
from google.oauth2 import service_account
|
16 |
import tiktoken
|
17 |
|
@@ -138,7 +139,8 @@ def setup_llm():
|
|
138 |
system = """You are an expert Tech Recruitor, your task is to analyse the Candidate profile and determine if it matches with the job details and provide a score(out of 10) indicating how compatible the
|
139 |
the profile is according to job.
|
140 |
First of all check the location of the candidate, if the location is not in the range of the job location then reject the candidate directly without any further analysis.
|
141 |
-
for example if the job location is New York and the candidate is in San Francisco then reject the candidate. Similarly for other states as well.
|
|
|
142 |
Try to ensure following points while estimating the candidate's fit score:
|
143 |
For education:
|
144 |
Tier1 - MIT, Stanford, CMU, UC Berkeley, Caltech, Harvard, IIT Bombay, IIT Delhi, Princeton, UIUC, University of Washington, Columbia, University of Chicago, Cornell, University of Michigan (Ann Arbor), UT Austin - Maximum points
|
@@ -247,7 +249,7 @@ def process_candidates_for_job(job_row, candidates_df, llm_chain=None):
|
|
247 |
"Locations": job_row.get("Locations", ""), "Tech_Stack": job_row["Tech Stack"], "Industry": job_row.get("Industry", "")
|
248 |
}
|
249 |
|
250 |
-
with st.spinner("
|
251 |
matching_candidates = get_matching_candidates(job_row["Tech Stack"], candidates_df)
|
252 |
|
253 |
if not matching_candidates:
|
@@ -287,9 +289,7 @@ def process_candidates_for_job(job_row, candidates_df, llm_chain=None):
|
|
287 |
f"**Selected Candidate:** [{response_dict['Name']}]({response_dict['LinkedIn']}) "
|
288 |
f"(Score: {response_dict['Fit Score']:.3f}, Location: {response_dict['Location']})"
|
289 |
)
|
290 |
-
|
291 |
-
# This st.write will also be visible during processing and cleared later.
|
292 |
-
st.write(f"Rejected candidate: {response_dict['Name']} with score: {response_dict['Fit Score']:.3f}, Location: {response_dict['Location']})")
|
293 |
candidates_progress.progress((i + 1) / len(matching_candidates))
|
294 |
|
295 |
candidates_progress.empty()
|
@@ -359,12 +359,12 @@ def main():
|
|
359 |
|
360 |
jobs_df = pd.DataFrame(job_data[1:], columns=job_data[0]).drop(["Link"], axis=1, errors='ignore')
|
361 |
jobs_df1 = jobs_df[["Company","Role","One liner","Locations","Tech Stack","Workplace","Industry","YOE"]]
|
|
|
362 |
candidates_df = pd.DataFrame(candidate_data[1:], columns=candidate_data[0]).fillna("Unknown")
|
363 |
candidates_df.drop_duplicates(subset=['LinkedIn URL'], keep='first', inplace=True)
|
364 |
|
365 |
with st.expander("Preview uploaded data"):
|
366 |
-
st.subheader("Jobs Data Preview"); st.dataframe(jobs_df1.head(
|
367 |
-
# st.subheader("Candidates Data Preview"); st.dataframe(candidates_df.head(3))
|
368 |
|
369 |
# Column mapping (simplified, ensure your CSVs have these exact names or adjust)
|
370 |
# candidates_df = candidates_df.rename(columns={...}) # Add if needed
|
@@ -375,244 +375,195 @@ def main():
|
|
375 |
st.error(f"Error processing files or data: {e}")
|
376 |
st.divider()
|
377 |
|
378 |
-
def display_job_selection(jobs_df, candidates_df, sh):
|
379 |
-
st.subheader("Select a job to
|
380 |
job_options = [f"{row['Role']} at {row['Company']}" for _, row in jobs_df.iterrows()]
|
381 |
-
|
382 |
if not job_options:
|
383 |
st.warning("No jobs found to display.")
|
384 |
return
|
385 |
|
386 |
-
|
387 |
-
|
|
|
388 |
job_row = jobs_df.iloc[selected_job_index]
|
389 |
-
job_row_stack = parse_tech_stack(job_row["Tech Stack"])
|
390 |
-
|
391 |
-
|
|
|
392 |
with col_job_details_display:
|
393 |
st.subheader(f"Job Details: {job_row['Role']}")
|
394 |
job_details_dict = {
|
395 |
-
"Company": job_row["Company"],
|
396 |
-
"
|
397 |
-
"
|
|
|
|
|
|
|
398 |
}
|
399 |
-
for key, value in job_details_dict.items():
|
|
|
400 |
|
401 |
-
# State keys
|
402 |
-
job_processed_key = f"job_{selected_job_index}_processed_successfully"
|
403 |
job_is_processing_key = f"job_{selected_job_index}_is_currently_processing"
|
|
|
|
|
404 |
|
405 |
-
#
|
406 |
-
if job_processed_key not in st.session_state: st.session_state[job_processed_key] = False
|
407 |
-
if job_is_processing_key not in st.session_state: st.session_state[job_is_processing_key] = False
|
408 |
-
|
409 |
sheet_name = f"{job_row['Role']} at {job_row['Company']}".strip()[:100]
|
410 |
worksheet_exists = False
|
411 |
-
existing_candidates_from_sheet = []
|
412 |
try:
|
413 |
-
|
|
|
|
|
414 |
worksheet_exists = True
|
415 |
-
|
416 |
-
if len(
|
417 |
-
existing_candidates_from_sheet =
|
418 |
-
except
|
419 |
pass
|
420 |
|
421 |
-
#
|
422 |
-
|
423 |
-
if not st.session_state.get(job_processed_key, False) or existing_candidates_from_sheet:
|
424 |
-
|
425 |
-
if existing_candidates_from_sheet and not st.session_state.get(job_is_processing_key, False) and not st.session_state.get(job_processed_key, False):
|
426 |
-
st.info(f"Processing ('{sheet_name}')")
|
427 |
-
|
428 |
col_find, col_stop = st.columns(2)
|
429 |
with col_find:
|
430 |
-
if st.button(
|
431 |
-
|
432 |
-
|
|
|
433 |
else:
|
434 |
st.session_state[job_is_processing_key] = True
|
435 |
-
st.session_state.stop_processing_flag = False
|
436 |
-
st.session_state.Selected_Candidates[selected_job_index] = []
|
437 |
-
st.session_state[job_processed_key] = False
|
438 |
st.rerun()
|
439 |
-
|
440 |
with col_stop:
|
441 |
-
if st.session_state
|
442 |
if st.button("STOP Processing", key=f"stop_btn_{selected_job_index}"):
|
443 |
-
st.session_state.stop_processing_flag = True
|
444 |
st.warning("Stop request sent. Processing will halt shortly.")
|
445 |
|
446 |
-
#
|
447 |
-
if st.session_state
|
448 |
-
with st.spinner(f"
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
if processed_candidates_list:
|
458 |
-
# Ensure Fit Score is float for reliable sorting
|
459 |
-
for cand in processed_candidates_list:
|
460 |
-
if 'Fit Score' in cand and isinstance(cand['Fit Score'], str):
|
461 |
-
try: cand['Fit Score'] = float(cand['Fit Score'])
|
462 |
-
except ValueError: cand['Fit Score'] = 0.0 # Default if conversion fails
|
463 |
-
elif 'Fit Score' not in cand:
|
464 |
-
cand['Fit Score'] = 0.0
|
465 |
-
|
466 |
-
processed_candidates_list.sort(key=lambda x: x.get("Fit Score", 0.0), reverse=True)
|
467 |
-
st.session_state.Selected_Candidates[selected_job_index] = processed_candidates_list
|
468 |
-
st.session_state[job_processed_key] = True # Mark as successfully processed
|
469 |
-
|
470 |
-
# Save to Google Sheet
|
471 |
try:
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
headers = list(processed_candidates_list[0].keys())
|
479 |
-
# Ensure all values are converted to strings for gspread
|
480 |
-
rows_to_write = [headers] + [[str(candidate.get(h, "")) for h in headers] for candidate in processed_candidates_list]
|
481 |
-
target_worksheet.clear()
|
482 |
-
target_worksheet.update('A1', rows_to_write)
|
483 |
st.success(f"Results saved to Google Sheet: '{sheet_name}'")
|
484 |
except Exception as e:
|
485 |
st.error(f"Error writing to Google Sheet '{sheet_name}': {e}")
|
486 |
else:
|
487 |
st.info("No suitable candidates found after processing.")
|
488 |
-
st.session_state.Selected_Candidates[selected_job_index] = []
|
489 |
-
st.session_state[job_processed_key] = True
|
490 |
-
else:
|
491 |
-
st.info("Processing was stopped by user.
|
492 |
-
st.session_state
|
493 |
-
st.session_state[
|
494 |
-
|
495 |
-
st.
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
btn.innerText = 'Copied!';
|
571 |
-
setTimeout(function() {{ btn.innerText = originalText; }}, 1500);
|
572 |
-
}}
|
573 |
-
}}, function(err) {{
|
574 |
-
console.error('Could not copy text: ', err);
|
575 |
-
alert('Failed to copy text. Please use Ctrl+C or your browser\\'s copy function.');
|
576 |
-
}});
|
577 |
-
}}
|
578 |
-
</script>
|
579 |
-
<button id="{button_unique_id}" onclick="copyToClipboard_{button_unique_id}()">π Copy Details</button>
|
580 |
-
"""
|
581 |
-
|
582 |
-
expander_cols = st.columns([0.82, 0.18])
|
583 |
-
with expander_cols[1]:
|
584 |
-
st.components.v1.html(copy_button_html, height=40)
|
585 |
-
|
586 |
-
with expander_cols[0]:
|
587 |
-
st.markdown(f"**Summary:** {candidate.get('summary', 'N/A')}")
|
588 |
-
st.markdown(f"**Current:** {candidate.get('Current Title & Company', 'N/A')}")
|
589 |
-
st.markdown(f"**Education:** {candidate.get('Educational Background', 'N/A')}")
|
590 |
-
st.markdown(f"**Experience:** {candidate.get('Years of Experience', 'N/A')}")
|
591 |
-
st.markdown(f"**Location:** {candidate.get('Location', 'N/A')}")
|
592 |
-
if 'LinkedIn' in candidate and candidate.get('LinkedIn'):
|
593 |
-
st.markdown(f"**[LinkedIn Profile]({candidate['LinkedIn']})**")
|
594 |
-
else:
|
595 |
-
st.markdown("**LinkedIn Profile:** N/A")
|
596 |
-
|
597 |
-
if 'justification' in candidate and candidate.get('justification'):
|
598 |
st.markdown("**Justification:**")
|
599 |
st.info(candidate['justification'])
|
600 |
-
|
601 |
-
|
602 |
-
st.info("No candidates met the criteria for this job after processing.")
|
603 |
-
|
604 |
-
# This "Reset" button is now governed by should_display_results_area
|
605 |
if st.button("Reset and Process Again", key=f"reset_btn_{selected_job_index}"):
|
606 |
st.session_state[job_processed_key] = False
|
607 |
-
st.session_state.pop(job_is_processing_key, None)
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
-
sh.worksheet(sheet_name).clear()
|
612 |
-
st.info(f"Cleared Google Sheet '{sheet_name}' as part of reset.")
|
613 |
-
except: pass # Ignore if sheet not found or error
|
614 |
st.rerun()
|
615 |
|
|
|
|
|
616 |
if __name__ == "__main__":
|
617 |
main()
|
618 |
|
|
|
12 |
from langchain_core.prompts import PromptTemplate # Not directly used in provided snippet
|
13 |
import gspread
|
14 |
import tempfile
|
15 |
+
import time
|
16 |
from google.oauth2 import service_account
|
17 |
import tiktoken
|
18 |
|
|
|
139 |
system = """You are an expert Tech Recruitor, your task is to analyse the Candidate profile and determine if it matches with the job details and provide a score(out of 10) indicating how compatible the
|
140 |
the profile is according to job.
|
141 |
First of all check the location of the candidate, if the location is not in the range of the job location then reject the candidate directly without any further analysis.
|
142 |
+
for example if the job location is New York and the candidate is in San Francisco or outside the new york state then directly reject the candidate without any further analysis. Similarly for other states as well.
|
143 |
+
|
144 |
Try to ensure following points while estimating the candidate's fit score:
|
145 |
For education:
|
146 |
Tier1 - MIT, Stanford, CMU, UC Berkeley, Caltech, Harvard, IIT Bombay, IIT Delhi, Princeton, UIUC, University of Washington, Columbia, University of Chicago, Cornell, University of Michigan (Ann Arbor), UT Austin - Maximum points
|
|
|
249 |
"Locations": job_row.get("Locations", ""), "Tech_Stack": job_row["Tech Stack"], "Industry": job_row.get("Industry", "")
|
250 |
}
|
251 |
|
252 |
+
with st.spinner("Finding matching candidates based on tech stack..."):
|
253 |
matching_candidates = get_matching_candidates(job_row["Tech Stack"], candidates_df)
|
254 |
|
255 |
if not matching_candidates:
|
|
|
289 |
f"**Selected Candidate:** [{response_dict['Name']}]({response_dict['LinkedIn']}) "
|
290 |
f"(Score: {response_dict['Fit Score']:.3f}, Location: {response_dict['Location']})"
|
291 |
)
|
292 |
+
|
|
|
|
|
293 |
candidates_progress.progress((i + 1) / len(matching_candidates))
|
294 |
|
295 |
candidates_progress.empty()
|
|
|
359 |
|
360 |
jobs_df = pd.DataFrame(job_data[1:], columns=job_data[0]).drop(["Link"], axis=1, errors='ignore')
|
361 |
jobs_df1 = jobs_df[["Company","Role","One liner","Locations","Tech Stack","Workplace","Industry","YOE"]]
|
362 |
+
jobs_df1 = jobs_df1.fillna("Unknown")
|
363 |
candidates_df = pd.DataFrame(candidate_data[1:], columns=candidate_data[0]).fillna("Unknown")
|
364 |
candidates_df.drop_duplicates(subset=['LinkedIn URL'], keep='first', inplace=True)
|
365 |
|
366 |
with st.expander("Preview uploaded data"):
|
367 |
+
st.subheader("Jobs Data Preview"); st.dataframe(jobs_df1.head(5))
|
|
|
368 |
|
369 |
# Column mapping (simplified, ensure your CSVs have these exact names or adjust)
|
370 |
# candidates_df = candidates_df.rename(columns={...}) # Add if needed
|
|
|
375 |
st.error(f"Error processing files or data: {e}")
|
376 |
st.divider()
|
377 |
|
378 |
+
def display_job_selection(jobs_df, candidates_df, sh):
|
379 |
+
st.subheader("Select a job to view potential matches")
|
380 |
job_options = [f"{row['Role']} at {row['Company']}" for _, row in jobs_df.iterrows()]
|
381 |
+
|
382 |
if not job_options:
|
383 |
st.warning("No jobs found to display.")
|
384 |
return
|
385 |
|
386 |
+
# Select job
|
387 |
+
selected_job_index = st.selectbox("Jobs:", range(len(job_options)),
|
388 |
+
format_func=lambda x: job_options[x], key="job_selectbox")
|
389 |
job_row = jobs_df.iloc[selected_job_index]
|
390 |
+
job_row_stack = parse_tech_stack(job_row["Tech Stack"])
|
391 |
+
|
392 |
+
# Display job details
|
393 |
+
col_job_details_display, _ = st.columns([2, 1])
|
394 |
with col_job_details_display:
|
395 |
st.subheader(f"Job Details: {job_row['Role']}")
|
396 |
job_details_dict = {
|
397 |
+
"Company": job_row["Company"],
|
398 |
+
"Role": job_row["Role"],
|
399 |
+
"Description": job_row.get("One liner", "N/A"),
|
400 |
+
"Locations": job_row.get("Locations", "N/A"),
|
401 |
+
"Industry": job_row.get("Industry", "N/A"),
|
402 |
+
"Tech Stack": display_tech_stack(job_row_stack)
|
403 |
}
|
404 |
+
for key, value in job_details_dict.items():
|
405 |
+
st.markdown(f"**{key}:** {value}")
|
406 |
|
407 |
+
# State keys
|
408 |
+
job_processed_key = f"job_{selected_job_index}_processed_successfully"
|
409 |
job_is_processing_key = f"job_{selected_job_index}_is_currently_processing"
|
410 |
+
st.session_state.setdefault(job_processed_key, False)
|
411 |
+
st.session_state.setdefault(job_is_processing_key, False)
|
412 |
|
413 |
+
# Check existing sheet data
|
|
|
|
|
|
|
414 |
sheet_name = f"{job_row['Role']} at {job_row['Company']}".strip()[:100]
|
415 |
worksheet_exists = False
|
416 |
+
existing_candidates_from_sheet = []
|
417 |
try:
|
418 |
+
with st.spinner("Checking for Ideal Candidates..."):
|
419 |
+
time .sleep(20) # Simulate some delay for checking
|
420 |
+
cand_ws = sh.worksheet(sheet_name)
|
421 |
worksheet_exists = True
|
422 |
+
data = cand_ws.get_all_values()
|
423 |
+
if len(data) > 1:
|
424 |
+
existing_candidates_from_sheet = data
|
425 |
+
except Exception:
|
426 |
pass
|
427 |
|
428 |
+
# Controls
|
429 |
+
if not st.session_state[job_processed_key] or existing_candidates_from_sheet:
|
|
|
|
|
|
|
|
|
|
|
430 |
col_find, col_stop = st.columns(2)
|
431 |
with col_find:
|
432 |
+
if st.button("Find Matching Candidates for this Job", key=f"find_btn_{selected_job_index}",
|
433 |
+
disabled=st.session_state[job_is_processing_key]):
|
434 |
+
if not os.environ.get("OPENAI_API_KEY") or st.session_state.llm_chain is None:
|
435 |
+
st.error("OpenAI API key not set or LLM not initialized.")
|
436 |
else:
|
437 |
st.session_state[job_is_processing_key] = True
|
438 |
+
st.session_state.stop_processing_flag = False
|
439 |
+
st.session_state.Selected_Candidates[selected_job_index] = []
|
440 |
+
st.session_state[job_processed_key] = False
|
441 |
st.rerun()
|
|
|
442 |
with col_stop:
|
443 |
+
if st.session_state[job_is_processing_key]:
|
444 |
if st.button("STOP Processing", key=f"stop_btn_{selected_job_index}"):
|
445 |
+
st.session_state.stop_processing_flag = True
|
446 |
st.warning("Stop request sent. Processing will halt shortly.")
|
447 |
|
448 |
+
# Processing
|
449 |
+
if st.session_state[job_is_processing_key]:
|
450 |
+
with st.spinner(f"Processing candidates for {job_row['Role']} at {job_row['Company']}..."):
|
451 |
+
processed_list = process_candidates_for_job(job_row, candidates_df, st.session_state.llm_chain)
|
452 |
+
st.session_state[job_is_processing_key] = False
|
453 |
+
|
454 |
+
if not st.session_state.get('stop_processing_flag', False):
|
455 |
+
if processed_list:
|
456 |
+
processed_list.sort(key=lambda x: x.get("Fit Score", 0.0), reverse=True)
|
457 |
+
st.session_state.Selected_Candidates[selected_job_index] = processed_list
|
458 |
+
st.session_state[job_processed_key] = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
459 |
try:
|
460 |
+
target_ws = sh.worksheet(sheet_name) if worksheet_exists else sh.add_worksheet(
|
461 |
+
title=sheet_name, rows=max(100, len(processed_list)+10), cols=20)
|
462 |
+
headers = list(processed_list[0].keys())
|
463 |
+
rows = [headers] + [[str(c.get(h, "")) for h in headers] for c in processed_list]
|
464 |
+
target_ws.clear()
|
465 |
+
target_ws.update('A1', rows)
|
|
|
|
|
|
|
|
|
|
|
466 |
st.success(f"Results saved to Google Sheet: '{sheet_name}'")
|
467 |
except Exception as e:
|
468 |
st.error(f"Error writing to Google Sheet '{sheet_name}': {e}")
|
469 |
else:
|
470 |
st.info("No suitable candidates found after processing.")
|
471 |
+
st.session_state.Selected_Candidates[selected_job_index] = []
|
472 |
+
st.session_state[job_processed_key] = True
|
473 |
+
else:
|
474 |
+
st.info("Processing was stopped by user.")
|
475 |
+
st.session_state[job_processed_key] = False
|
476 |
+
st.session_state.Selected_Candidates[selected_job_index] = []
|
477 |
+
st.session_state.pop('stop_processing_flag', None)
|
478 |
+
st.rerun()
|
479 |
+
|
480 |
+
# Display results
|
481 |
+
should_display = False
|
482 |
+
final_candidates = []
|
483 |
+
if not st.session_state[job_is_processing_key]:
|
484 |
+
if st.session_state[job_processed_key]:
|
485 |
+
should_display = True
|
486 |
+
final_candidates = st.session_state.Selected_Candidates[selected_job_index]
|
487 |
+
elif existing_candidates_from_sheet:
|
488 |
+
should_display = True
|
489 |
+
headers = existing_candidates_from_sheet[0]
|
490 |
+
for row in existing_candidates_from_sheet[1:]:
|
491 |
+
cand = {headers[i]: row[i] if i < len(row) else None for i in range(len(headers))}
|
492 |
+
try: cand['Fit Score'] = float(cand.get('Fit Score',0))
|
493 |
+
except: cand['Fit Score'] = 0.0
|
494 |
+
final_candidates.append(cand)
|
495 |
+
final_candidates.sort(key=lambda x: x.get('Fit Score',0.0), reverse=True)
|
496 |
+
if not st.session_state[job_processed_key]:
|
497 |
+
st.info(f"Displaying: '{sheet_name}'.")
|
498 |
+
|
499 |
+
if should_display:
|
500 |
+
# Prepare combined text for Copy All
|
501 |
+
combined_text = ""
|
502 |
+
for cand in final_candidates:
|
503 |
+
combined_text += f"Name: {cand.get('Name','N/A')}\nLinkedIn URL: {cand.get('LinkedIn','N/A')}\n\n"
|
504 |
+
|
505 |
+
# Header and Copy All button side by side
|
506 |
+
col_title, col_copyall = st.columns([3,1])
|
507 |
+
with col_title:
|
508 |
+
st.subheader("Selected Candidates")
|
509 |
+
with col_copyall:
|
510 |
+
# Use HTML component to reliably include script and button
|
511 |
+
import json
|
512 |
+
html = f'''
|
513 |
+
<button id="copy-all-btn">π Copy All</button>
|
514 |
+
<script>
|
515 |
+
const combinedText = {json.dumps(combined_text)};
|
516 |
+
document.getElementById("copy-all-btn").onclick = () => {{
|
517 |
+
navigator.clipboard.writeText(combinedText);
|
518 |
+
}};
|
519 |
+
</script>
|
520 |
+
'''
|
521 |
+
st.components.v1.html(html, height=60)
|
522 |
+
|
523 |
+
# Token usage
|
524 |
+
if st.session_state.get(job_processed_key) and (
|
525 |
+
st.session_state.get('total_input_tokens',0) > 0 or st.session_state.get('total_output_tokens',0) > 0):
|
526 |
+
display_token_usage()
|
527 |
+
|
528 |
+
# List individual candidates
|
529 |
+
for i, candidate in enumerate(final_candidates):
|
530 |
+
score = candidate.get('Fit Score',0.0)
|
531 |
+
score_display = f"{score:.3f}" if isinstance(score,(int,float)) else score
|
532 |
+
exp_title = f"{i+1}. {candidate.get('Name','N/A')} (Score: {score_display})"
|
533 |
+
with st.expander(exp_title):
|
534 |
+
text_copy = f"Candidate: {candidate.get('Name','N/A')}\nLinkedIn: {candidate.get('LinkedIn','N/A')}\n"
|
535 |
+
btn = f"copy_btn_job{selected_job_index}_cand{i}"
|
536 |
+
js = f'''
|
537 |
+
<script>
|
538 |
+
function copyToClipboard_{btn}() {{ navigator.clipboard.writeText(`{text_copy}`); }}
|
539 |
+
</script>
|
540 |
+
<button onclick="copyToClipboard_{btn}()">π Copy Details</button>
|
541 |
+
'''
|
542 |
+
cols = st.columns([0.82,0.18])
|
543 |
+
with cols[1]: st.components.v1.html(js, height=40)
|
544 |
+
with cols[0]:
|
545 |
+
st.markdown(f"**Summary:** {candidate.get('summary','N/A')}")
|
546 |
+
st.markdown(f"**Current:** {candidate.get('Current Title & Company','N/A')}")
|
547 |
+
st.markdown(f"**Education:** {candidate.get('Educational Background','N/A')}")
|
548 |
+
st.markdown(f"**Experience:** {candidate.get('Years of Experience','N/A')}")
|
549 |
+
st.markdown(f"**Location:** {candidate.get('Location','N/A')}")
|
550 |
+
if candidate.get('LinkedIn'):
|
551 |
+
st.markdown(f"**[LinkedIn Profile]({candidate['LinkedIn']})**")
|
552 |
+
if candidate.get('justification'):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
553 |
st.markdown("**Justification:**")
|
554 |
st.info(candidate['justification'])
|
555 |
+
|
556 |
+
# Reset
|
|
|
|
|
|
|
557 |
if st.button("Reset and Process Again", key=f"reset_btn_{selected_job_index}"):
|
558 |
st.session_state[job_processed_key] = False
|
559 |
+
st.session_state.pop(job_is_processing_key, None)
|
560 |
+
st.session_state.Selected_Candidates.pop(selected_job_index, None)
|
561 |
+
try: sh.worksheet(sheet_name).clear()
|
562 |
+
except: pass
|
|
|
|
|
|
|
563 |
st.rerun()
|
564 |
|
565 |
+
|
566 |
+
|
567 |
if __name__ == "__main__":
|
568 |
main()
|
569 |
|