File size: 9,286 Bytes
d0fa643
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import streamlit as st
import pandas as pd
import os
from datetime import datetime

# Import backend agents
from backend.agents.remoteok_agent import fetch_remoteok_jobs
from backend.agents.rapidapi_linkedin import fetch_linkedin_jobs_stub
from backend.agents.rapidapi_upwork import fetch_upwork_jobs_stub
from backend.agents.rapidapi_freelancer import fetch_freelancer_jobs_stub
from backend.agents.resume_parser import parse_resume
from backend.agents.matcher import get_embedding, calculate_match_score
from backend.agents.resume_gen import generate_custom_resume, generate_cover_letter

# --- Page Configuration ---
st.set_page_config(
    page_title="MATCHHIVE | AI Job Matcher",
    page_icon="🐝",
    layout="wide",
    initial_sidebar_state="expanded",
)

# --- Styling ---
st.markdown("""
<style>
    .stProgress > div > div > div > div {
        background-image: linear-gradient(to right, #f9a825, #fdd835);
    }
    .stButton>button {
        border-radius: 20px;
        border: 1px solid #f9a825;
        background-color: #ffffff;
        color: #f9a825;
        transition: all 0.2s ease-in-out;
    }
    .stButton>button:hover {
        background-color: #f9a825;
        color: #ffffff;
        border: 1px solid #f9a825;
    }
    .stButton>button:focus {
        box-shadow: 0 0 0 0.2rem rgba(249, 168, 37, 0.5);
    }
    h1, h2, h3 {
        color: #2c3e50;
    }
</style>
""", unsafe_allow_html=True)


# --- State Management ---
if 'resume_text' not in st.session_state:
    st.session_state.resume_text = ""
if 'jobs_df' not in st.session_state:
    st.session_state.jobs_df = pd.DataFrame()
if 'groq_api_key' not in st.session_state:
    try:
        st.session_state.groq_api_key = st.secrets["GROQ_API_KEY"]
    except (KeyError, FileNotFoundError):
        st.session_state.groq_api_key = ""

# --- Helper Functions ---
def safe_get_embedding(text, api_key):
    """Safely get embeddings and handle potential errors."""
    if not api_key:
        st.error("Groq API key is not set. Please add it in the sidebar.")
        return None
    try:
        return get_embedding(text, api_key)
    except Exception as e:
        st.error(f"Failed to generate embeddings: {e}")
        return None

# --- Sidebar ---
with st.sidebar:
    st.image("https://i.imgur.com/S5t8k2S.png", width=100) # Placeholder logo
    st.title("MATCHHIVE 🐝")
    st.markdown("Your AI-powered career co-pilot.")

    st.header("1. Upload Your Resume")
    uploaded_file = st.file_uploader(
        "Upload your resume (PDF or DOCX)",
        type=["pdf", "docx"],
        label_visibility="collapsed"
    )
    if uploaded_file:
        with st.spinner("Parsing resume..."):
            try:
                st.session_state.resume_text = parse_resume(uploaded_file)
                st.success("Resume parsed successfully!")
                st.text_area("Parsed Resume Text", st.session_state.resume_text, height=150, disabled=True)
            except Exception as e:
                st.error(f"Error parsing resume: {e}")

    st.header("2. API Keys")
    groq_api_key_input = st.text_input(
        "Groq API Key",
        type="password",
        placeholder="gsk_...",
        value=st.session_state.groq_api_key
    )
    if groq_api_key_input:
        st.session_state.groq_api_key = groq_api_key_input

    st.markdown("---")
    st.markdown("Developed by an AI Engineer.")
    st.markdown("Powered by Streamlit & Groq.")


# --- Main Application ---
st.title("AI Job Aggregator & Matcher")
st.markdown("Upload your resume, and we'll find the best job matches for you from across the web.")

# --- Job Fetching Controls ---
st.header("Find Your Next Opportunity")
col1, col2, col3 = st.columns([2,1,1])

with col1:
    search_query = st.text_input("Job Title / Keywords", "Software Engineer", help="Enter keywords like 'Python Developer', 'Data Scientist', etc.")

with col2:
    st.markdown("<div style='height: 28px;'></div>", unsafe_allow_html=True) # Vertical alignment
    fetch_button = st.button("πŸ” Fetch & Match Jobs", use_container_width=True, type="primary")

if fetch_button:
    if not st.session_state.resume_text:
        st.warning("Please upload your resume first.")
    elif not st.session_state.groq_api_key:
        st.error("Please enter your Groq API key in the sidebar.")
    else:
        with st.spinner("Fetching jobs and calculating matches... This may take a moment."):
            # Fetch jobs from all sources
            remoteok_jobs = fetch_remoteok_jobs(limit=20) # We limit to keep it fast for the MVP
            linkedin_jobs = fetch_linkedin_jobs_stub()
            upwork_jobs = fetch_upwork_jobs_stub()
            freelancer_jobs = fetch_freelancer_jobs_stub()

            all_jobs = remoteok_jobs + linkedin_jobs + upwork_jobs + freelancer_jobs
            jobs_df = pd.DataFrame(all_jobs)

            # Filter jobs based on search query
            if search_query:
                jobs_df = jobs_df[jobs_df['description'].str.contains(search_query, case=False, na=False) |
                                  jobs_df['title'].str.contains(search_query, case=False, na=False)]

            if not jobs_df.empty:
                # Calculate match scores
                resume_embedding = safe_get_embedding(st.session_state.resume_text, st.session_state.groq_api_key)
                if resume_embedding is not None:
                    job_descriptions = jobs_df['description'].tolist()
                    job_embeddings = [safe_get_embedding(desc, st.session_state.groq_api_key) for desc in job_descriptions]

                    scores = []
                    for job_emb in job_embeddings:
                        if job_emb is not None:
                            score = calculate_match_score(resume_embedding, job_emb)
                            scores.append(int(score * 100))
                        else:
                            scores.append(0)
                    jobs_df['match_score'] = scores
                    jobs_df = jobs_df.sort_values(by='match_score', ascending=False).reset_index(drop=True)
                    st.session_state.jobs_df = jobs_df
                    st.success(f"Found and matched {len(jobs_df)} jobs!")
            else:
                st.warning("No jobs found for the given criteria. Try a different query.")
                st.session_state.jobs_df = pd.DataFrame()


# --- Display Matched Jobs ---
if not st.session_state.jobs_df.empty:
    st.header("Top Job Matches")

    for index, job in st.session_state.jobs_df.iterrows():
        st.markdown("---")
        score = job['match_score']

        # Determine color based on score
        if score > 80:
            color = "#27ae60" # Green
        elif score > 60:
            color = "#f39c12" # Yellow
        else:
            color = "#c0392b" # Red

        col_title, col_score = st.columns([4, 1])

        with col_title:
            st.subheader(f"{job['title']}")
            st.caption(f"🏒 **Company:** {job['company']} | πŸ“ **Location:** {job['location']} |  piattaforma: {job['source']}")

        with col_score:
            st.markdown(f"**Match Score: <span style='color:{color}; font-size: 1.2em;'>{score}%</span>**", unsafe_allow_html=True)
            progress_value = score / 100.0
            st.progress(progress_value)


        with st.expander("View Job Details & Generate Application Materials"):
            st.markdown(f"**Description:**")
            # We truncate the description for cleaner display
            st.markdown(f"<div style='max-height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; padding: 10px; border-radius: 5px;'>{job['description']}</div>", unsafe_allow_html=True)

            gen_col1, gen_col2 = st.columns(2)
            with gen_col1:
                if st.button("πŸ“„ Generate Custom Resume", key=f"resume_{index}", use_container_width=True):
                    with st.spinner("Tailoring your resume..."):
                        custom_resume = generate_custom_resume(
                            st.session_state.resume_text,
                            job['description'],
                            st.session_state.groq_api_key
                        )
                        st.text_area("Your Tailored Resume", value=custom_resume, height=300, key=f"custom_resume_text_{index}")

            with gen_col2:
                if st.button("βœ‰οΈ Generate Cover Letter", key=f"cover_{index}", use_container_width=True):
                    with st.spinner("Crafting your cover letter..."):
                        cover_letter = generate_cover_letter(
                            st.session_state.resume_text,
                            job['description'],
                            st.session_state.groq_api_key
                        )
                        st.text_area("Your Custom Cover Letter", value=cover_letter, height=300, key=f"cover_letter_text_{index}")

            st.markdown(f"<a href='{job['url']}' target='_blank' style='display: inline-block; padding: 10px 20px; background-color: #f9a825; color: white; text-align: center; text-decoration: none; border-radius: 20px; font-weight: bold;'>πŸš€ Apply Now</a>", unsafe_allow_html=True)

else:
    st.info("Your matched jobs will appear here once you fetch them.")