Alpha108 commited on
Commit
d0fa643
Β·
verified Β·
1 Parent(s): f2228aa

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -0
app.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ from datetime import datetime
5
+
6
+ # Import backend agents
7
+ from backend.agents.remoteok_agent import fetch_remoteok_jobs
8
+ from backend.agents.rapidapi_linkedin import fetch_linkedin_jobs_stub
9
+ from backend.agents.rapidapi_upwork import fetch_upwork_jobs_stub
10
+ from backend.agents.rapidapi_freelancer import fetch_freelancer_jobs_stub
11
+ from backend.agents.resume_parser import parse_resume
12
+ from backend.agents.matcher import get_embedding, calculate_match_score
13
+ from backend.agents.resume_gen import generate_custom_resume, generate_cover_letter
14
+
15
+ # --- Page Configuration ---
16
+ st.set_page_config(
17
+ page_title="MATCHHIVE | AI Job Matcher",
18
+ page_icon="🐝",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded",
21
+ )
22
+
23
+ # --- Styling ---
24
+ st.markdown("""
25
+ <style>
26
+ .stProgress > div > div > div > div {
27
+ background-image: linear-gradient(to right, #f9a825, #fdd835);
28
+ }
29
+ .stButton>button {
30
+ border-radius: 20px;
31
+ border: 1px solid #f9a825;
32
+ background-color: #ffffff;
33
+ color: #f9a825;
34
+ transition: all 0.2s ease-in-out;
35
+ }
36
+ .stButton>button:hover {
37
+ background-color: #f9a825;
38
+ color: #ffffff;
39
+ border: 1px solid #f9a825;
40
+ }
41
+ .stButton>button:focus {
42
+ box-shadow: 0 0 0 0.2rem rgba(249, 168, 37, 0.5);
43
+ }
44
+ h1, h2, h3 {
45
+ color: #2c3e50;
46
+ }
47
+ </style>
48
+ """, unsafe_allow_html=True)
49
+
50
+
51
+ # --- State Management ---
52
+ if 'resume_text' not in st.session_state:
53
+ st.session_state.resume_text = ""
54
+ if 'jobs_df' not in st.session_state:
55
+ st.session_state.jobs_df = pd.DataFrame()
56
+ if 'groq_api_key' not in st.session_state:
57
+ try:
58
+ st.session_state.groq_api_key = st.secrets["GROQ_API_KEY"]
59
+ except (KeyError, FileNotFoundError):
60
+ st.session_state.groq_api_key = ""
61
+
62
+ # --- Helper Functions ---
63
+ def safe_get_embedding(text, api_key):
64
+ """Safely get embeddings and handle potential errors."""
65
+ if not api_key:
66
+ st.error("Groq API key is not set. Please add it in the sidebar.")
67
+ return None
68
+ try:
69
+ return get_embedding(text, api_key)
70
+ except Exception as e:
71
+ st.error(f"Failed to generate embeddings: {e}")
72
+ return None
73
+
74
+ # --- Sidebar ---
75
+ with st.sidebar:
76
+ st.image("https://i.imgur.com/S5t8k2S.png", width=100) # Placeholder logo
77
+ st.title("MATCHHIVE 🐝")
78
+ st.markdown("Your AI-powered career co-pilot.")
79
+
80
+ st.header("1. Upload Your Resume")
81
+ uploaded_file = st.file_uploader(
82
+ "Upload your resume (PDF or DOCX)",
83
+ type=["pdf", "docx"],
84
+ label_visibility="collapsed"
85
+ )
86
+ if uploaded_file:
87
+ with st.spinner("Parsing resume..."):
88
+ try:
89
+ st.session_state.resume_text = parse_resume(uploaded_file)
90
+ st.success("Resume parsed successfully!")
91
+ st.text_area("Parsed Resume Text", st.session_state.resume_text, height=150, disabled=True)
92
+ except Exception as e:
93
+ st.error(f"Error parsing resume: {e}")
94
+
95
+ st.header("2. API Keys")
96
+ groq_api_key_input = st.text_input(
97
+ "Groq API Key",
98
+ type="password",
99
+ placeholder="gsk_...",
100
+ value=st.session_state.groq_api_key
101
+ )
102
+ if groq_api_key_input:
103
+ st.session_state.groq_api_key = groq_api_key_input
104
+
105
+ st.markdown("---")
106
+ st.markdown("Developed by an AI Engineer.")
107
+ st.markdown("Powered by Streamlit & Groq.")
108
+
109
+
110
+ # --- Main Application ---
111
+ st.title("AI Job Aggregator & Matcher")
112
+ st.markdown("Upload your resume, and we'll find the best job matches for you from across the web.")
113
+
114
+ # --- Job Fetching Controls ---
115
+ st.header("Find Your Next Opportunity")
116
+ col1, col2, col3 = st.columns([2,1,1])
117
+
118
+ with col1:
119
+ search_query = st.text_input("Job Title / Keywords", "Software Engineer", help="Enter keywords like 'Python Developer', 'Data Scientist', etc.")
120
+
121
+ with col2:
122
+ st.markdown("<div style='height: 28px;'></div>", unsafe_allow_html=True) # Vertical alignment
123
+ fetch_button = st.button("πŸ” Fetch & Match Jobs", use_container_width=True, type="primary")
124
+
125
+ if fetch_button:
126
+ if not st.session_state.resume_text:
127
+ st.warning("Please upload your resume first.")
128
+ elif not st.session_state.groq_api_key:
129
+ st.error("Please enter your Groq API key in the sidebar.")
130
+ else:
131
+ with st.spinner("Fetching jobs and calculating matches... This may take a moment."):
132
+ # Fetch jobs from all sources
133
+ remoteok_jobs = fetch_remoteok_jobs(limit=20) # We limit to keep it fast for the MVP
134
+ linkedin_jobs = fetch_linkedin_jobs_stub()
135
+ upwork_jobs = fetch_upwork_jobs_stub()
136
+ freelancer_jobs = fetch_freelancer_jobs_stub()
137
+
138
+ all_jobs = remoteok_jobs + linkedin_jobs + upwork_jobs + freelancer_jobs
139
+ jobs_df = pd.DataFrame(all_jobs)
140
+
141
+ # Filter jobs based on search query
142
+ if search_query:
143
+ jobs_df = jobs_df[jobs_df['description'].str.contains(search_query, case=False, na=False) |
144
+ jobs_df['title'].str.contains(search_query, case=False, na=False)]
145
+
146
+ if not jobs_df.empty:
147
+ # Calculate match scores
148
+ resume_embedding = safe_get_embedding(st.session_state.resume_text, st.session_state.groq_api_key)
149
+ if resume_embedding is not None:
150
+ job_descriptions = jobs_df['description'].tolist()
151
+ job_embeddings = [safe_get_embedding(desc, st.session_state.groq_api_key) for desc in job_descriptions]
152
+
153
+ scores = []
154
+ for job_emb in job_embeddings:
155
+ if job_emb is not None:
156
+ score = calculate_match_score(resume_embedding, job_emb)
157
+ scores.append(int(score * 100))
158
+ else:
159
+ scores.append(0)
160
+ jobs_df['match_score'] = scores
161
+ jobs_df = jobs_df.sort_values(by='match_score', ascending=False).reset_index(drop=True)
162
+ st.session_state.jobs_df = jobs_df
163
+ st.success(f"Found and matched {len(jobs_df)} jobs!")
164
+ else:
165
+ st.warning("No jobs found for the given criteria. Try a different query.")
166
+ st.session_state.jobs_df = pd.DataFrame()
167
+
168
+
169
+ # --- Display Matched Jobs ---
170
+ if not st.session_state.jobs_df.empty:
171
+ st.header("Top Job Matches")
172
+
173
+ for index, job in st.session_state.jobs_df.iterrows():
174
+ st.markdown("---")
175
+ score = job['match_score']
176
+
177
+ # Determine color based on score
178
+ if score > 80:
179
+ color = "#27ae60" # Green
180
+ elif score > 60:
181
+ color = "#f39c12" # Yellow
182
+ else:
183
+ color = "#c0392b" # Red
184
+
185
+ col_title, col_score = st.columns([4, 1])
186
+
187
+ with col_title:
188
+ st.subheader(f"{job['title']}")
189
+ st.caption(f"🏒 **Company:** {job['company']} | πŸ“ **Location:** {job['location']} | piattaforma: {job['source']}")
190
+
191
+ with col_score:
192
+ st.markdown(f"**Match Score: <span style='color:{color}; font-size: 1.2em;'>{score}%</span>**", unsafe_allow_html=True)
193
+ progress_value = score / 100.0
194
+ st.progress(progress_value)
195
+
196
+
197
+ with st.expander("View Job Details & Generate Application Materials"):
198
+ st.markdown(f"**Description:**")
199
+ # We truncate the description for cleaner display
200
+ 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)
201
+
202
+ gen_col1, gen_col2 = st.columns(2)
203
+ with gen_col1:
204
+ if st.button("πŸ“„ Generate Custom Resume", key=f"resume_{index}", use_container_width=True):
205
+ with st.spinner("Tailoring your resume..."):
206
+ custom_resume = generate_custom_resume(
207
+ st.session_state.resume_text,
208
+ job['description'],
209
+ st.session_state.groq_api_key
210
+ )
211
+ st.text_area("Your Tailored Resume", value=custom_resume, height=300, key=f"custom_resume_text_{index}")
212
+
213
+ with gen_col2:
214
+ if st.button("βœ‰οΈ Generate Cover Letter", key=f"cover_{index}", use_container_width=True):
215
+ with st.spinner("Crafting your cover letter..."):
216
+ cover_letter = generate_cover_letter(
217
+ st.session_state.resume_text,
218
+ job['description'],
219
+ st.session_state.groq_api_key
220
+ )
221
+ st.text_area("Your Custom Cover Letter", value=cover_letter, height=300, key=f"cover_letter_text_{index}")
222
+
223
+ 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)
224
+
225
+ else:
226
+ st.info("Your matched jobs will appear here once you fetch them.")