ak0601 commited on
Commit
9c845bd
Β·
verified Β·
1 Parent(s): b3ff1bd

Update src/app_job_copy_1.py

Browse files
Files changed (1) hide show
  1. 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("Sourcing candidates based on tech stack..."):
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
- else:
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(3))
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): # 'sh' is the Google Sheets client
379
- st.subheader("Select a job to Source for 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
- selected_job_index = st.selectbox("Jobs:", range(len(job_options)), format_func=lambda x: job_options[x], key="job_selectbox")
387
-
 
388
  job_row = jobs_df.iloc[selected_job_index]
389
- job_row_stack = parse_tech_stack(job_row["Tech Stack"]) # Assuming parse_tech_stack is defined
390
-
391
- col_job_details_display, _ = st.columns([2,1])
 
392
  with col_job_details_display:
393
  st.subheader(f"Job Details: {job_row['Role']}")
394
  job_details_dict = {
395
- "Company": job_row["Company"], "Role": job_row["Role"], "Description": job_row.get("One liner", "N/A"),
396
- "Locations": job_row.get("Locations", "N/A"), "Industry": job_row.get("Industry", "N/A"),
397
- "Tech Stack": display_tech_stack(job_row_stack) # Assuming display_tech_stack is defined
 
 
 
398
  }
399
- for key, value in job_details_dict.items(): st.markdown(f"**{key}:** {value}")
 
400
 
401
- # State keys for the selected job
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
- # Initialize states if they don't exist for this job
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 = [] # This will store raw data from sheet
412
  try:
413
- cand_worksheet = sh.worksheet(sheet_name)
 
 
414
  worksheet_exists = True
415
- existing_data = cand_worksheet.get_all_values() # Get all values as list of lists
416
- if len(existing_data) > 1: # Has data beyond header
417
- existing_candidates_from_sheet = existing_data # Store raw data
418
- except gspread.exceptions.WorksheetNotFound:
419
  pass
420
 
421
- # --- Processing Control Area ---
422
- # Show controls if not successfully processed in this session OR if sheet exists (allow re-process/overwrite)
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(f"Find Matching Candidates for this Job", key=f"find_btn_{selected_job_index}", disabled=st.session_state.get(job_is_processing_key, False)):
431
- if not os.environ.get("OPENAI_API_KEY") or st.session_state.llm_chain is None: # Assuming llm_chain is in session_state
432
- st.error("OpenAI API key not set or LLM not initialized. Please check sidebar.")
 
433
  else:
434
  st.session_state[job_is_processing_key] = True
435
- st.session_state.stop_processing_flag = False # Reset for new run, assuming stop_processing_flag is used
436
- st.session_state.Selected_Candidates[selected_job_index] = [] # Clear previous run for this job
437
- st.session_state[job_processed_key] = False # Mark as not successfully processed yet for this attempt
438
  st.rerun()
439
-
440
  with col_stop:
441
- if st.session_state.get(job_is_processing_key, False): # Show STOP only if "Find" was clicked and currently processing
442
  if st.button("STOP Processing", key=f"stop_btn_{selected_job_index}"):
443
- st.session_state.stop_processing_flag = True # Assuming stop_processing_flag is used
444
  st.warning("Stop request sent. Processing will halt shortly.")
445
 
446
- # --- Actual Processing Logic ---
447
- if st.session_state.get(job_is_processing_key, False):
448
- with st.spinner(f"Sourcing candidates for {job_row['Role']} at {job_row['Company']}..."):
449
- # Assuming process_candidates_for_job is defined and handles stop_processing_flag
450
- processed_candidates_list = process_candidates_for_job(
451
- job_row, candidates_df, st.session_state.llm_chain # Assuming llm_chain from session_state
452
- )
453
-
454
- st.session_state[job_is_processing_key] = False # Mark as no longer actively processing
455
-
456
- if not st.session_state.get('stop_processing_flag', False): # If processing was NOT stopped
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
- target_worksheet = None
473
- if not worksheet_exists:
474
- target_worksheet = sh.add_worksheet(title=sheet_name, rows=max(100, len(processed_candidates_list) + 10), cols=20)
475
- else:
476
- target_worksheet = sh.worksheet(sheet_name)
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 # Mark as processed, even if no results
490
- else: # If processing WAS stopped
491
- st.info("Processing was stopped by user. Results (if any) were not saved. You can try processing again.")
492
- st.session_state.Selected_Candidates[selected_job_index] = [] # Clear any partial results
493
- st.session_state[job_processed_key] = False # Not successfully processed
494
-
495
- st.session_state.pop('stop_processing_flag', None) # Clean up flag
496
- st.rerun() # Rerun to update UI based on new state
497
-
498
- # --- Display Results Area ---
499
- should_display_results_area = False
500
- final_candidates_to_display = [] # Initialize to ensure it's always defined
501
-
502
- if st.session_state.get(job_is_processing_key, False):
503
- should_display_results_area = False # Not if actively processing
504
- elif st.session_state.get(job_processed_key, False): # If successfully processed in this session
505
- should_display_results_area = True
506
- final_candidates_to_display = st.session_state.Selected_Candidates.get(selected_job_index, [])
507
- elif existing_candidates_from_sheet: # If not processed in this session, but sheet has data
508
- should_display_results_area = True
509
- headers = existing_candidates_from_sheet[0]
510
- parsed_sheet_candidates = []
511
- for row_idx, row_data in enumerate(existing_candidates_from_sheet[1:]): # Skip header row
512
- candidate_dict = {}
513
- for col_idx, header_name in enumerate(headers):
514
- candidate_dict[header_name] = row_data[col_idx] if col_idx < len(row_data) else None
515
-
516
- # Convert Fit Score from string to float for consistent handling
517
- if 'Fit Score' in candidate_dict and isinstance(candidate_dict['Fit Score'], str):
518
- try:
519
- candidate_dict['Fit Score'] = float(candidate_dict['Fit Score'])
520
- except ValueError:
521
- st.warning(f"Could not convert Fit Score '{candidate_dict['Fit Score']}' to float for candidate in sheet row {row_idx+2}.")
522
- candidate_dict['Fit Score'] = 0.0 # Default if conversion fails
523
- elif 'Fit Score' not in candidate_dict:
524
- candidate_dict['Fit Score'] = 0.0
525
-
526
-
527
- parsed_sheet_candidates.append(candidate_dict)
528
- final_candidates_to_display = sorted(parsed_sheet_candidates, key=lambda x: x.get("Fit Score", 0.0), reverse=True)
529
- if not st.session_state.get(job_processed_key, False): # Inform if loading from sheet and not explicitly processed
530
- st.info(f"Displaying: '{sheet_name}'.")
531
-
532
- if should_display_results_area:
533
- st.subheader("Selected Candidates")
534
-
535
- # Display token usage if it was just processed (job_processed_key is True and tokens exist)
536
- if st.session_state.get(job_processed_key, False) and \
537
- (st.session_state.get('total_input_tokens', 0) > 0 or st.session_state.get('total_output_tokens', 0) > 0):
538
- display_token_usage() # Assuming display_token_usage is defined
539
-
540
- if final_candidates_to_display:
541
- for i, candidate in enumerate(final_candidates_to_display):
542
- score_display = candidate.get('Fit Score', 'N/A')
543
- if isinstance(score_display, (float, int)):
544
- score_display = f"{score_display:.3f}"
545
- # If score_display is still a string (e.g. 'N/A' or failed float conversion), it will be displayed as is.
546
-
547
- expander_title = f"{i+1}. {candidate.get('Name', 'N/A')} (Score: {score_display})"
548
-
549
- with st.expander(expander_title):
550
- text_to_copy = f"""Candidate: {candidate.get('Name', 'N/A')} (Score: {score_display})
551
- Summary: {candidate.get('summary', 'N/A')}
552
- Current: {candidate.get('Current Title & Company', 'N/A')}
553
- Education: {candidate.get('Educational Background', 'N/A')}
554
- Experience: {candidate.get('Years of Experience', 'N/A')}
555
- Location: {candidate.get('Location', 'N/A')}
556
- LinkedIn: {candidate.get('LinkedIn', 'N/A')}
557
- Justification: {candidate.get('justification', 'N/A')}
558
- """
559
- js_text_to_copy = json.dumps(text_to_copy)
560
- button_unique_id = f"copy_btn_job{selected_job_index}_cand{i}"
561
-
562
- copy_button_html = f"""
563
- <script>
564
- function copyToClipboard_{button_unique_id}() {{
565
- const textToCopy = {js_text_to_copy};
566
- navigator.clipboard.writeText(textToCopy).then(function() {{
567
- const btn = document.getElementById('{button_unique_id}');
568
- if (btn) {{ // Check if button exists
569
- const originalText = btn.innerText;
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
- elif st.session_state.get(job_processed_key, False): # Processed but no candidates
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
- if selected_job_index in st.session_state.Selected_Candidates:
609
- del st.session_state.Selected_Candidates[selected_job_index]
610
- try:
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