SonFox2920's picture
Update app.py
d0d63f6 verified
import streamlit as st
import google.generativeai as genai
from pypdf import PdfReader
import pandas as pd
import requests
from io import BytesIO
import os
import time
import re
from html import unescape
import json
from docx import Document
from datetime import datetime
from analyze import dashboard
from bs4 import BeautifulSoup
from config import new_schema
# Function definitions
def is_valid_url(url):
pattern = r'https://hiring\.base\.vn/opening/candidates/(\d+)\?stage=(\d+)$'
return re.match(pattern, url) is not None
def load_job_descriptions(csv_file):
df = pd.read_csv(csv_file)
return dict(zip(df['Position'], df['Job_Description']))
def get_pdf_text_from_url(url):
try:
response = requests.get(url)
response.raise_for_status()
with BytesIO(response.content) as f:
reader = PdfReader(f)
text = ""
for page in reader.pages:
text += page.extract_text() + " "
return ' '.join(text.split()) # Xóa khoảng trắng thừa nếu có
except Exception as e:
st.error(f"Lỗi khi tải hoặc trích xuất văn bản từ URL {url}: {str(e)}")
return None
def get_docx_text_from_url(url):
try:
response = requests.get(url)
response.raise_for_status()
with BytesIO(response.content) as f:
document = Document(f)
text = ""
for para in document.paragraphs:
text += para.text + "\n"
return ' '.join(text.split()) # Xóa khoảng trắng thừa nếu có
except Exception as e:
print(f"Lỗi khi tải hoặc trích xuất văn bản từ URL {url}: {str(e)}")
return None
# Hàm kiểm tra định dạng file và chọn hàm tương ứng
def get_cv_text_from_url(cv_url):
if not isinstance(cv_url, str):
print(f"Invalid URL format: {cv_url}. Expected string, got {type(cv_url)}.")
return None
cv_url = cv_url.strip() # Remove any leading/trailing whitespace
if not cv_url: # Check if the URL is empty after stripping
print("Empty URL provided.")
return None
if cv_url.lower().endswith('.pdf'):
return get_pdf_text_from_url(cv_url)
elif cv_url.lower().endswith('.docx'):
return get_docx_text_from_url(cv_url)
else:
print(f"Unsupported file format for URL: {cv_url}")
return None
def get_gemini_response(prompt, content):
model = genai.GenerativeModel('models/gemini-1.5-flash-latest',
generation_config={
"response_mime_type": "application/json",
"response_schema": new_schema
})
response = model.generate_content(prompt + content)
response_json = json.loads(response.text)
time.sleep(3)
return response_json
def extract_ids_from_url(url):
match = re.search(r'candidates/(\d+)\?stage=(\d+)', url)
if match:
return match.group(1), match.group(2)
return None, None
# Updated function to fetch candidates using stage_ids
def fetch_candidates_by_stages(opening_id, access_token):
"""
Fetch candidates for specific stages of a job opening
Parameters:
opening_id (str): The ID of the opening
access_token (str): The API access token
stage_ids (list): List of stage IDs to filter candidates
Returns:
dict: API response with candidate data
"""
api_url = "https://hiring.base.vn/publicapi/v2/candidate/list"
# Prepare the payload
payload = {
'access_token': access_token,
'opening_id': opening_id,
'num_per_page': '10000',
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(api_url, headers=headers, data=payload)
return response.json()
def fetch_jd(opening_id, access_token):
api_url = "https://hiring.base.vn/publicapi/v2/opening/get"
payload = {
'access_token': access_token,
'id': opening_id,
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(api_url, headers=headers, data=payload)
# Parse the JSON response
json_response = response.json()
# Get the 'content' field
html_content = json_response.get('opening', {}).get('content', '')
# Use BeautifulSoup to convert HTML content to plain text
soup = BeautifulSoup(html_content, "html.parser")
plain_text = soup.get_text()
return plain_text
# New function to get active stages for an opening
def get_opening_stages(opening_id, access_token):
"""Get active stages for a specific job opening"""
api_url = "https://hiring.base.vn/publicapi/v2/opening/get"
payload = {
'access_token': access_token,
'id': opening_id,
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(api_url, headers=headers, data=payload)
if response.status_code == 200:
data = response.json()
active_stages = [
{"id": stage["id"], "name": stage["name"]}
for stage in data.get("opening", {}).get("stats", {}).get("stages", [])
if stage.get("state") == "active"
]
return pd.DataFrame(active_stages)
else:
st.error(f"Lỗi khi lấy danh sách giai đoạn: {response.status_code} - {response.text}")
return pd.DataFrame()
def extract_message(evaluations):
"""Extract text content from HTML evaluations"""
if isinstance(evaluations, list) and len(evaluations) > 0:
raw_html = evaluations[0].get('content', '')
soup = BeautifulSoup(raw_html, "html.parser")
return " ".join(soup.stripped_strings)
return None
def process_data(data):
if 'candidates' not in data:
st.error("Không tìm thấy ứng viên trong phản hồi.")
return None
df = pd.DataFrame(data['candidates'])
# Convert cvs to first element if it exists
if 'cvs' in df.columns:
df['cvs'] = df['cvs'].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else None)
# Clean title if it exists
if 'title' in df.columns:
df['title'] = df['title'].apply(lambda x: re.sub(r'<.*?>', '', x) if isinstance(x, str) else x)
# Unescape name if it exists
if 'name' in df.columns:
df['name'] = df['name'].apply(lambda x: unescape(x) if isinstance(x, str) else x)
# Drop DOB columns if they exist
dob_columns = ['dob_day', 'dob_month', 'dob_year']
df_columns = df.columns.tolist()
for col in dob_columns:
if col in df_columns:
df = df.drop(columns=[col])
# Extract review from evaluations if it exists
if 'evaluations' in df.columns:
df['review'] = df['evaluations'].apply(extract_message)
# Filter out rows without CVs if cvs column exists
if 'cvs' in df.columns:
df = df[df['cvs'].notnull()]
# Drop columns with all null values
df = df.dropna(axis=1, how='all')
# List of columns to select (if they exist in the DataFrame)
columns_to_select = ['id', 'name', 'gender', 'cvs', 'email', 'phone', 'form', 'stage_id']
if 'review' in df.columns:
columns_to_select.append('review')
# Select only columns that exist in the DataFrame
available_columns = [col for col in columns_to_select if col in df.columns]
df = df[available_columns]
# Process form data if it exists
if 'form' in df.columns:
form_data_list = df['form']
# Convert each row in 'form' column to a dictionary
form_df_list = []
for form_data in form_data_list:
if isinstance(form_data, list):
data_dict = {item['id']: item['value'] for item in form_data}
form_df_list.append(data_dict)
else:
form_df_list.append({})
# Create new DataFrame from list of dictionaries
form_df_transformed = pd.DataFrame(form_df_list)
# Merge the original DataFrame (without 'form' column) with the transformed form data
df_merged = pd.concat([df.drop(columns=['form']), form_df_transformed], axis=1)
selected_df = df_merged
else:
selected_df = df
df_cleaned = selected_df.dropna(axis=1, how='all') # Xóa cột chứa toàn bộ giá trị None
# Nếu muốn xóa cả cột chứa toàn bộ chuỗi rỗng
df_cleaned = df_cleaned.loc[:, ~(df_cleaned == '').all()]
return df_cleaned
# New function to get active job openings
def get_base_openings(access_token):
"""Retrieve active job openings from Base API"""
url = "https://hiring.base.vn/publicapi/v2/opening/list"
payload = {'access_token': access_token}
response = requests.post(url, data=payload)
if response.status_code == 200:
data = response.json()
openings = data.get('openings', [])
filtered_openings = [
{"id": opening['id'], "name": opening['name']}
for opening in openings if opening.get('status') == '10'
]
return pd.DataFrame(filtered_openings)
else:
st.error(f"Lỗi khi lấy danh sách vị trí tuyển dụng: {response.status_code} - {response.text}")
return pd.DataFrame()
# Function to convert empty strings to None
def convert_empty_to_none(df):
"""Convert empty strings to None in a pandas DataFrame"""
# Replace empty strings with None in the entire DataFrame
return df.replace('', None)
# Main application
st.set_page_config(page_title="Công Cụ Đánh Giá CV và Lấy Dữ Liệu Công Việc", layout="wide")
st.title("🚀 Công Cụ Đánh Giá CV và Lấy Dữ Liệu Công Việc")
# Configure Google API
api_key = os.getenv('GOOGLE_API_KEY')
if api_key:
genai.configure(api_key=api_key)
else:
st.error("Không tìm thấy GOOGLE_API_KEY trong biến môi trường. Vui lòng cấu hình trước khi sử dụng ứng dụng.")
st.stop()
# Get BASE API Key
access_token = os.getenv('BASE_API_KEY')
if not access_token:
st.error("Không tìm thấy BASE_API_KEY trong biến môi trường. Vui lòng cấu hình trước khi sử dụng ứng dụng.")
st.stop()
st.sidebar.header("📚 Hướng dẫn sử dụng")
st.sidebar.markdown("""
### 👋 Chào mừng bạn đến với Công cụ Đánh giá CV!
Công cụ này giúp bạn:
1. **🔍 Lấy thông tin ứng viên tự động từ Base.vn**
2. **📊 Đánh giá CV theo tiêu chí công việc**
3. **📈 Phân tích dữ liệu tuyển dụng qua Dashboard**
""")
with st.sidebar.expander("🔍 Cách lấy thông tin và đánh giá CV", expanded=False):
st.markdown("""
### Bước 1: Chọn nguồn dữ liệu
- Chuyển đến tab "Lấy Dữ Liệu Ứng Viên và Đánh giá CV"
- Chọn vị trí tuyển dụng từ danh sách dropdown
### Bước 2: Chọn giai đoạn tuyển dụng
- Chọn một hoặc nhiều giai đoạn tuyển dụng để lọc ứng viên
### Bước 3: Tải tài liệu bổ sung (tùy chọn)
- Nếu có, tải lên file PDF/DOCX chứa thông tin bổ sung
### Bước 4: Xử lý dữ liệu
- Nhấp "Lấy Thông Tin Ứng Viên" để bắt đầu
- Hệ thống sẽ tự động đánh giá CV dựa trên JD
### Bước 5: Xem và lưu kết quả
- Kết quả đánh giá hiển thị ở dạng bảng
- Tải xuống file CSV để lưu trữ và phân tích
""")
with st.sidebar.expander("📈 Cách sử dụng Dashboard phân tích", expanded=False):
st.markdown("""
### Bước 1: Mở tab Dashboard
- Chuyển đến tab "Dashboard"
### Bước 2: Tải lên dữ liệu
- Tải lên file CSV đã lưu từ chức năng đánh giá CV
### Bước 3: Xem báo cáo và biểu đồ
- Xem thống kê tổng quan về ứng viên
- Phân tích điểm số theo từng tiêu chí
- So sánh hiệu quả giữa các nguồn tuyển dụng
""")
st.sidebar.warning("""
**⚠️ Lưu ý quan trọng:**
- Đảm bảo bạn đã đăng nhập vào hệ thống Base.vn
- Công cụ này hỗ trợ quyết định, không thay thế đánh giá chuyên môn
- Kết quả đánh giá dựa trên AI có thể cần kiểm tra lại
- Bảo mật thông tin ứng viên theo quy định PDPA
""")
st.sidebar.info("""
**💡 Mẹo sử dụng:**
- Tải lên tài liệu bổ sung để có đánh giá chính xác hơn
- Chọn các giai đoạn tuyển dụng phù hợp để lọc ứng viên
- Tải xuống CSV sau mỗi lần đánh giá để lưu trữ
""")
st.sidebar.success("✨ Chúc bạn tuyển dụng hiệu quả!")
# Tabs for different functionalities
tab1, tab2 = st.tabs(["🔍 Lấy Dữ Liệu Ứng Viên và 📊 Đánh giá CV", "📈 Dashboard"])
with tab1:
st.header("🔍 Lấy Dữ Liệu Ứng Viên")
# Add file upload feature
st.subheader("📁 Tải lên tài liệu bổ sung (tùy chọn)")
uploaded_file = st.file_uploader("Tải lên file PDF hoặc DOCX để cung cấp thông tin bổ sung", type=["pdf", "docx"])
additional_info = ""
if uploaded_file is not None:
with st.spinner("⏳ Đang xử lý file tải lên..."):
# Create BytesIO object from uploaded file
file_bytes = BytesIO(uploaded_file.getvalue())
# Process the file based on type
if uploaded_file.name.lower().endswith('.pdf'):
try:
reader = PdfReader(file_bytes)
text = ""
for page in reader.pages:
text += page.extract_text() + " "
additional_info = ' '.join(text.split()) # Remove extra whitespace
st.success(f"✅ Đã xử lý file PDF: {uploaded_file.name}")
st.text_area("Nội dung file PDF: ",text, disabled=True)
except Exception as e:
st.error(f"❌ Lỗi khi xử lý file PDF: {str(e)}")
elif uploaded_file.name.lower().endswith('.docx'):
try:
document = Document(file_bytes)
text = ""
for para in document.paragraphs:
text += para.text + "\n"
additional_info = ' '.join(text.split()) # Remove extra whitespace
st.success(f"✅ Đã xử lý file DOCX: {uploaded_file.name}")
st.text_area("Nội dung file WORD: ",text, disabled=True)
except Exception as e:
st.error(f"❌ Lỗi khi xử lý file DOCX: {str(e)}")
# Get job openings for dropdown
job_openings = get_base_openings(access_token)
if not job_openings.empty:
selected_job = st.selectbox(
"🏢 Chọn vị trí tuyển dụng:",
options=job_openings['name'].tolist(),
key="job_selection"
)
# Get the opening_id for the selected job
selected_opening_id = job_openings[job_openings['name'] == selected_job]['id'].iloc[0]
# Get stages for the selected opening
stages_df = get_opening_stages(selected_opening_id, access_token)
if not stages_df.empty:
# Multi-select for stages
selected_stages = st.multiselect(
"📋 Chọn giai đoạn tuyển dụng:",
options=stages_df['name'].tolist(),
default=stages_df['name'].tolist()[:1], # Chỉ chọn mục đầu tiên mặc định
key="stages_selection"
)
# Get the stage_ids for the selected stages
selected_stage_ids = stages_df[stages_df['name'].isin(selected_stages)]['id'].tolist()
else:
st.warning("Không thể tải danh sách giai đoạn tuyển dụng")
selected_stage_ids = []
else:
st.warning("Không thể tải danh sách vị trí tuyển dụng")
selected_job = None
selected_opening_id = None
selected_stage_ids = []
# Process button
if st.button("🔎 Lấy Thông Tin Ứng Viên"):
if selected_opening_id and selected_stage_ids:
# Fetch candidates by stages
with st.spinner("⏳ Đang lấy thông tin ứng viên từ Base.vn..."):
raw_data = fetch_candidates_by_stages(selected_opening_id, access_token)
data = process_data(raw_data)
if selected_stage_ids:
data = data[data['stage_id'].isin(selected_stage_ids)]
jd = fetch_jd(selected_opening_id, access_token)
elif selected_opening_id:
# If no stages selected, fetch all candidates for the opening
with st.spinner("⏳ Đang lấy thông tin tất cả ứng viên từ vị trí này..."):
raw_data = fetch_candidates_by_stages(selected_opening_id, access_token)
data = process_data(raw_data)
jd = fetch_jd(selected_opening_id, access_token)
else:
st.error("Vui lòng chọn vị trí tuyển dụng")
st.stop()
# Convert empty strings to None
data = convert_empty_to_none(data)
if data is not None and not data.empty:
# Get stage names and create a stage mapping dictionary
stages_mapping = dict(zip(stages_df['id'].astype(str), stages_df['name']))
# Create a preview dataframe with key information
preview_df = data[['name', 'email', 'phone', 'stage_id']].copy()
# Add stage name column
preview_df['stage_name'] = preview_df['stage_id'].astype(str).map(stages_mapping)
# Rename columns for display
preview_df.rename(columns={
'name': 'Tên ứng viên',
'email': 'Email',
'phone': 'Số điện thoại',
'stage_id': 'Mã giai đoạn',
'stage_name': 'Giai đoạn'
}, inplace=True)
# Display the preview dataframe
st.subheader(f"📋 Danh sách {len(preview_df)} ứng viên sẽ được đánh giá")
st.dataframe(preview_df)
st.success(f"✅ Đã lấy thông tin {len(data)} ứng viên thành công!")
st.header("📊 Đánh giá và Lọc CV")
# Continue with the existing evaluation code
results = []
progress_bar = st.progress(0)
# Inside the loop where each CV is processed
for i, (_, row) in enumerate(data.iterrows()):
name = row['name']
cv_url = row['cvs']
cv_text = get_cv_text_from_url(cv_url)
# Initialize empty strings for review and form data
review_text = ""
form_data_text = ""
# Add review data if available
if 'review' in row and pd.notna(row['review']) and row['review'] != "":
review_text = f"ĐÁNH GIÁ CỦA NHÀ TUYỂN DỤNG: {row['review'].strip()}"
# Add form data if available
form_data_fields = []
for column in row.index:
# Skip non-form data columns
if column not in ['id', 'name', 'gender', 'cvs', 'email', 'phone', 'review', 'stage_id']:
# Explicitly check for None, empty string, and NaN
if pd.notna(row[column]) and row[column] is not None and row[column] != "":
form_data_fields.append(f"{column}: {row[column]}")
if form_data_fields:
form_data_text = "THÔNG TIN TỪ FORM ỨNG TUYỂN: " + " ".join(form_data_fields)
if cv_text:
# Add the additional information from uploaded file
additional_info_text = ""
if additional_info and additional_info.strip():
additional_info_text = f"THÔNG TIN BỔ SUNG TỪ TÀI LIỆU ĐÃ TẢI LÊN: {additional_info.strip()}"
# Build prompt components and filter out empty ones
prompt_components = [
"Bạn là chuyên gia nhân sự với 15 năm kinh nghiệm đánh giá CV. Nhiệm vụ của bạn là đánh giá chi tiết và khách quan CV dưới đây dựa trên mô tả công việc và đặc thù tuyển dụng tại A Plus.",
"### ĐẶC THÙ TUYỂN DỤNG TẠI A PLUS:",
"- Công ty đang trong quá trình chuyển đổi mạnh mẽ.",
"- Tìm kiếm ứng viên có khả năng **thích ứng với công nghệ, AI và sự thay đổi**.",
"- Ưu tiên **tiềm năng phát triển hơn kinh nghiệm**, đề cao tinh thần khởi nghiệp.",
"- Không phù hợp với ứng viên **chỉ tìm kiếm sự ổn định hoặc phúc lợi cao ngay từ đầu**.",
"- Tiêu chí đánh giá: 40% năng lực chuyên môn, 30% phù hợp văn hóa, 20% khả năng học hỏi & đổi mới, 10% điểm cộng & điểm trừ đặc biệt.",
"### MÔ TẢ CÔNG VIỆC:",
f"{jd.strip()}" if jd and jd.strip() else None,
"### CÁC YẾU TỐ QUAN TRỌNG KHI ĐÁNH GIÁ CV:",
"- **Năng lực chuyên môn:** Kinh nghiệm làm việc với nhà cung cấp, xử lý chứng từ, theo dõi tiến độ mua hàng, làm việc với đơn vị logistics.",
"- **Phù hợp văn hóa A Plus:** Cẩn thận & minh bạch, tư duy linh hoạt & chủ động, phù hợp mô hình cung ứng tinh gọn.",
"- **Khả năng học hỏi & đổi mới:** Chủ động học hỏi, thích ứng với thay đổi, tư duy cải tiến & tối ưu công việc.",
"- **Điểm cộng & điểm trừ:** Các điểm mạnh nổi bật (ngoại ngữ, phân tích dữ liệu, ERP...) và rủi ro tiềm ẩn/điểm yếu lớn.",
"### HƯỚNG DẪN CHẤM ĐIỂM:",
"1. **Năng lực chuyên môn (40%)**",
" - 5 – Xuất sắc: Đã có kinh nghiệm thực tế, làm việc hiệu quả, chủ động xử lý vấn đề.",
" - 3-4 – Có kinh nghiệm: Đã làm trong lĩnh vực liên quan nhưng cần rèn luyện thêm.",
" - 1-2 – Chưa có kinh nghiệm: Thiếu kiến thức hoặc chưa từng làm công việc tương tự.",
"2. **Phù hợp văn hóa (30%)**",
" - 5 – Xuất sắc: Tinh thần trách nhiệm cao, linh hoạt, chủ động, phù hợp hoàn toàn với mô hình A Plus.",
" - 3-4 – Phù hợp tương đối: Có tinh thần làm việc tốt nhưng cần kiểm chứng thêm.",
" - 1-2 – Chưa phù hợp: Thiếu sự chủ động, khó thích nghi với văn hóa công ty.",
"3. **Khả năng học hỏi & đổi mới (20%)**",
" - 5 – Rất tốt: Luôn chủ động học hỏi, tiếp thu nhanh công nghệ và quy trình mới.",
" - 3-4 – Ổn: Có khả năng học hỏi nhưng cần thời gian thích nghi.",
" - 1-2 – Yếu: Ít chủ động học tập, khó tiếp thu thay đổi.",
"4. **Điểm cộng & điểm trừ (10%)**",
" - Điểm cộng (Thêm điểm nếu có kỹ năng đặc biệt như ngoại ngữ, ERP, phân tích dữ liệu, v.v.).",
" - Điểm trừ (Trừ điểm nếu có hạn chế lớn như không linh hoạt, thiếu kinh nghiệm thực tế, v.v.).",
"### HƯỚNG DẪN ĐÁNH GIÁ:",
"1. Phân tích kỹ CV và so sánh với từng yêu cầu trong JD.",
"2. Đối với mỗi tiêu chí, đưa ra điểm số từ 1-5 dựa trên mức độ phù hợp.",
"3. Đưa ra nhận xét khách quan, nêu rõ điểm mạnh và điểm cần cải thiện.",
"4. Xem xét các tiêu chí như **độ nhảy việc, tốc độ thăng tiến, khả năng làm việc với nhiều đối tác**.",
"5. Áp dụng **điểm cộng & điểm trừ** khi phân tích CV.",
"6. Nếu có thông tin từ form ứng tuyển hoặc nhận xét của nhà tuyển dụng, hãy kết hợp vào phân tích.",
"7. Vui lòng trả về kết quả đánh giá **CHÍNH XÁC theo định dạng JSON**.",
f"### NỘI DUNG CV CỦA ỨNG VIÊN {name}: {cv_text.strip()}" if cv_text and cv_text.strip() else None,
review_text if review_text.strip() else None,
form_data_text if form_data_text.strip() else None,
additional_info_text if additional_info_text.strip() else None,
]
# Filter out None values
prompt = " ".join([component for component in prompt_components if component is not None])
try:
response = get_gemini_response(prompt, cv_text)
# Thay đổi công thức tính điểm tổng thể
main_CV_score = round((response["nang_luc_chuyen_mon"]*0.4 +
response["phu_hop_van_hoa"]*0.3 +
response["kha_nang_hoc_hoi"]*0.2 +
(response["diem_cong"] - response["diem_tru"])*0.1), 2)
# Đảm bảo điểm không bị âm
main_CV_score = max(0, main_CV_score)
uv = {
'Tên ứng viên': name,
'Năng lực chuyên môn': response["nang_luc_chuyen_mon"],
'Phù hợp văn hóa': response["phu_hop_van_hoa"],
'Khả năng học hỏi': response["kha_nang_hoc_hoi"],
'Điểm cộng': response["diem_cong"],
'Điểm trừ': response["diem_tru"],
'Điểm tổng quát': main_CV_score,
'Tóm tắt': response["tom_tat"]
}
results.append(uv)
except Exception as e:
st.error(f"❌ Lỗi khi xử lý CV từ {cv_url}: {str(e)}")
progress_bar.progress(min(1.0, (i + 1) / len(data)))
if results:
st.subheader("📊 Kết quả đánh giá CV")
# Create a DataFrame from the evaluation results
df_results = pd.DataFrame(results)
# Merge the original DataFrame with evaluation results on the 'name' column
final_df = pd.merge(data, df_results, left_on='name', right_on='Tên ứng viên', how='inner')
# Drop the duplicate 'Tên ứng viên (gốc)' column
final_df.drop(columns=['Tên ứng viên'], inplace=True)
# Get stage names and create a stage mapping dictionary
stages_mapping = dict(zip(stages_df['id'].astype(str), stages_df['name']))
# Add stage name column if stage_id exists
if 'stage_id' in final_df.columns:
final_df['Giai đoạn'] = final_df['stage_id'].astype(str).map(stages_mapping)
# Rename the columns to Vietnamese, keeping only one 'Tên ứng viên' column
final_df.rename(columns={
'id': 'Mã ứng viên',
'name': "Tên ứng viên",
'email': 'Email',
'status': 'Trạng thái',
'cvs': 'Link CV',
'stage_id': 'Mã giai đoạn'
}, inplace=True)
opening_id = selected_opening_id
# Add link to candidate profile
final_df['Link ứng viên'] = final_df['Mã ứng viên'].apply(
lambda x: f"https://hiring.base.vn/opening/{opening_id}?candidate={x}"
)
# Loại bỏ các cột không có bất kỳ dữ liệu nào hoặc chỉ chứa giá trị rỗng
final_df = final_df.dropna(axis=1, how='all') # Xóa cột nếu tất cả giá trị đều là NaN
final_df = final_df.loc[:, (final_df != "").any(axis=0)] # Xóa cột nếu tất cả giá trị đều là ""
st.header("📋 Dữ liệu chi tiết")
st.dataframe(final_df)
# Add download button for the results
csv = final_df.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="📥 Tải xuống kết quả đánh giá CSV",
data=csv,
file_name="ket_qua_danh_gia_cv.csv",
mime="text/csv",
)
else:
st.warning("⚠️ Không có kết quả nào được tạo. Vui lòng kiểm tra API key và thử lại.")
else:
st.error("Không thể lấy dữ liệu hoặc không tìm thấy ứng viên nào.")
with tab2:
dashboard()
st.markdown("---")
st.markdown("🚀 Powered by Streamlit | 💼 Created for HR professionals")