Skip to content

Commit bcfcca4

Browse files
committed
Add beautiful HTML report generator for job search results
- Create view_results.html: modern, responsive template with filtering - Add generate_html_report.py: automated HTML report generation - Add convert_my_results.py: quick script to convert downloaded JSON - Update workflow to generate HTML report automatically - Features: match score badges, platform tags, filtering, mobile-friendly - HTML report included in artifacts for easy viewing
1 parent dd328ef commit bcfcca4

File tree

4 files changed

+862
-3
lines changed

4 files changed

+862
-3
lines changed

.github/workflows/weekly_job_search.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,17 @@ jobs:
7272
# Get most recent results file
7373
if [ -d job_search_results ]; then
7474
LATEST_RESULT=$(ls -t job_search_results/job_search_*.json 2>/dev/null | head -1)
75-
if [ -n "$LATEST_RESULT" ] && [ -f display_results.py ]; then
76-
python display_results.py "$LATEST_RESULT" || echo "⚠️ Could not generate report"
75+
if [ -n "$LATEST_RESULT" ]; then
76+
# Generate text report
77+
if [ -f display_results.py ]; then
78+
python display_results.py "$LATEST_RESULT" || echo "⚠️ Could not generate text report"
79+
fi
80+
# Generate HTML report
81+
if [ -f generate_html_report.py ]; then
82+
python generate_html_report.py "$LATEST_RESULT" job_search_report.html || echo "⚠️ Could not generate HTML report"
83+
fi
7784
else
78-
echo "⚠️ No results file or display_results.py found"
85+
echo "⚠️ No results file found"
7986
fi
8087
else
8188
echo "⚠️ job_search_results directory not found"
@@ -99,6 +106,7 @@ jobs:
99106
job_search_results/**
100107
job_search_reports/**
101108
resume_templates/**
109+
job_search_report.html
102110
if-no-files-found: ignore
103111
retention-days: 30
104112

convert_my_results.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Quick script to convert your downloaded job search JSON into a beautiful HTML report
4+
Just run: python convert_my_results.py <your_json_file.json>
5+
"""
6+
import json
7+
import sys
8+
from pathlib import Path
9+
from datetime import datetime
10+
11+
def create_html_report(json_data):
12+
"""Create a beautiful HTML report from JSON data"""
13+
14+
profile = json_data.get('profile', {})
15+
jobs = json_data.get('jobs', [])
16+
summary = json_data.get('summary', {})
17+
search_date = json_data.get('search_date', '')
18+
19+
# Count platforms
20+
platforms = set(j.get('platform', '') for j in jobs)
21+
22+
html = f"""<!DOCTYPE html>
23+
<html lang="en">
24+
<head>
25+
<meta charset="UTF-8">
26+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
27+
<title>Neuro Job Search Results - {search_date[:10]}</title>
28+
<style>
29+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
30+
body {{
31+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
32+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
33+
padding: 20px;
34+
min-height: 100vh;
35+
}}
36+
.container {{
37+
max-width: 1200px;
38+
margin: 0 auto;
39+
background: white;
40+
border-radius: 20px;
41+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
42+
overflow: hidden;
43+
}}
44+
.header {{
45+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
46+
color: white;
47+
padding: 40px;
48+
text-align: center;
49+
}}
50+
.header h1 {{ font-size: 2.5em; margin-bottom: 10px; }}
51+
.header .date {{ opacity: 0.9; font-size: 1.1em; }}
52+
.stats {{
53+
display: grid;
54+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
55+
gap: 20px;
56+
padding: 30px 40px;
57+
background: #f8f9fa;
58+
border-bottom: 2px solid #e9ecef;
59+
}}
60+
.stat-card {{
61+
text-align: center;
62+
padding: 20px;
63+
background: white;
64+
border-radius: 10px;
65+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
66+
}}
67+
.stat-card .number {{ font-size: 2.5em; font-weight: bold; color: #667eea; }}
68+
.stat-card .label {{ color: #6c757d; margin-top: 5px; }}
69+
.profile-section {{ padding: 30px 40px; background: #fff; border-bottom: 2px solid #e9ecef; }}
70+
.profile-section h2 {{ color: #667eea; margin-bottom: 15px; }}
71+
.tags {{ display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }}
72+
.tag {{
73+
background: #667eea;
74+
color: white;
75+
padding: 5px 15px;
76+
border-radius: 15px;
77+
font-size: 0.9em;
78+
}}
79+
.jobs-section {{ padding: 30px 40px; }}
80+
.jobs-section h2 {{ color: #667eea; margin-bottom: 20px; font-size: 1.8em; }}
81+
.job-card {{
82+
background: white;
83+
border: 2px solid #e9ecef;
84+
border-radius: 12px;
85+
padding: 25px;
86+
margin-bottom: 20px;
87+
position: relative;
88+
transition: all 0.3s;
89+
}}
90+
.job-card:hover {{
91+
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.2);
92+
border-color: #667eea;
93+
transform: translateY(-2px);
94+
}}
95+
.match-badge {{
96+
position: absolute;
97+
top: 20px;
98+
right: 20px;
99+
padding: 8px 15px;
100+
border-radius: 20px;
101+
font-weight: bold;
102+
color: white;
103+
}}
104+
.match-high {{ background: #28a745; }}
105+
.match-medium {{ background: #ffc107; color: #333; }}
106+
.match-low {{ background: #dc3545; }}
107+
.job-title {{ font-size: 1.5em; font-weight: 700; margin-bottom: 10px; padding-right: 120px; }}
108+
.job-company {{ font-size: 1.1em; color: #667eea; margin-bottom: 10px; font-weight: 600; }}
109+
.job-meta {{ color: #6c757d; margin: 10px 0; }}
110+
.job-description {{ color: #495057; line-height: 1.6; margin: 15px 0; }}
111+
.platform-badge {{
112+
display: inline-block;
113+
padding: 4px 10px;
114+
border-radius: 12px;
115+
font-size: 0.8em;
116+
font-weight: 600;
117+
margin: 10px 5px 10px 0;
118+
}}
119+
.platform-wellfound {{ background: #ffeaa7; }}
120+
.platform-indeed {{ background: #74b9ff; }}
121+
.platform-linkedin {{ background: #0077b5; color: white; }}
122+
.platform-startup_board {{ background: #fd79a8; color: white; }}
123+
.platform-remoteok {{ background: #a29bfe; color: white; }}
124+
.btn {{
125+
display: inline-block;
126+
padding: 12px 24px;
127+
background: #667eea;
128+
color: white;
129+
text-decoration: none;
130+
border-radius: 8px;
131+
font-weight: 600;
132+
margin-top: 15px;
133+
transition: all 0.3s;
134+
}}
135+
.btn:hover {{ background: #5568d3; transform: translateY(-1px); }}
136+
.footer {{ background: #f8f9fa; padding: 30px; text-align: center; color: #6c757d; }}
137+
@media (max-width: 768px) {{
138+
.match-badge {{ position: static; margin-bottom: 10px; }}
139+
.job-title {{ padding-right: 0; }}
140+
}}
141+
</style>
142+
</head>
143+
<body>
144+
<div class="container">
145+
<div class="header">
146+
<h1>🎯 Neuro Job Search Results</h1>
147+
<p class="date">Generated: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p>
148+
</div>
149+
150+
<div class="stats">
151+
<div class="stat-card">
152+
<div class="number">{summary.get('total_jobs', 0)}</div>
153+
<div class="label">Total Jobs</div>
154+
</div>
155+
<div class="stat-card">
156+
<div class="number">{summary.get('high_matches', 0)}</div>
157+
<div class="label">High Matches (>70%)</div>
158+
</div>
159+
<div class="stat-card">
160+
<div class="number">{summary.get('medium_matches', 0)}</div>
161+
<div class="label">Medium Matches</div>
162+
</div>
163+
<div class="stat-card">
164+
<div class="number">{len(platforms)}</div>
165+
<div class="label">Platforms</div>
166+
</div>
167+
</div>
168+
169+
<div class="profile-section">
170+
<h2>👤 Your Profile</h2>
171+
<div>
172+
<strong>Name:</strong> {profile.get('name', 'N/A')}<br>
173+
<strong>Experience:</strong> {profile.get('experience_level', 'N/A')}<br>
174+
<strong>Remote Preference:</strong> {'Yes ✅' if profile.get('remote_preference') else 'No'}
175+
</div>
176+
<div style="margin-top: 15px;">
177+
<strong>Target Roles:</strong>
178+
<div class="tags">
179+
{''.join(f'<span class="tag">{role}</span>' for role in profile.get('target_roles', []))}
180+
</div>
181+
</div>
182+
<div style="margin-top: 15px;">
183+
<strong>Skills:</strong>
184+
<div class="tags">
185+
{''.join(f'<span class="tag">{skill}</span>' for skill in profile.get('skills', []))}
186+
</div>
187+
</div>
188+
<div style="margin-top: 15px;">
189+
<strong>Locations:</strong>
190+
<div class="tags">
191+
{''.join(f'<span class="tag">{loc}</span>' for loc in profile.get('locations', []))}
192+
</div>
193+
</div>
194+
</div>
195+
196+
<div class="jobs-section">
197+
<h2>💼 Job Listings ({len(jobs)} jobs found)</h2>
198+
"""
199+
200+
# Sort jobs by match score
201+
sorted_jobs = sorted(jobs, key=lambda x: x.get('match_score', 0), reverse=True)
202+
203+
for job in sorted_jobs:
204+
match_score = job.get('match_score', 0)
205+
match_class = 'match-high' if match_score >= 70 else 'match-medium' if match_score >= 40 else 'match-low'
206+
207+
html += f"""
208+
<div class="job-card">
209+
<div class="match-badge {match_class}">{int(match_score)}% Match</div>
210+
<h3 class="job-title">{job.get('title', 'N/A')}</h3>
211+
<div class="job-company">{job.get('company', 'N/A')}</div>
212+
<div class="job-meta">
213+
📍 {job.get('location', 'N/A')}
214+
<span class="platform-badge platform-{job.get('platform', '').replace(' ', '_')}">{job.get('platform', 'N/A')}</span>
215+
</div>
216+
<div class="job-description">{job.get('description', 'No description available')}</div>
217+
<a href="{job.get('url', '#')}" target="_blank" class="btn">View Job →</a>
218+
</div>
219+
"""
220+
221+
html += """
222+
</div>
223+
224+
<div class="footer">
225+
<p><strong>Neuro</strong> - The Intent-Driven Language for AI</p>
226+
<p style="margin-top: 10px;">🔗 <a href="https://github.com/ElaMCB/Neuro" style="color: #667eea;">GitHub</a></p>
227+
</div>
228+
</div>
229+
</body>
230+
</html>
231+
"""
232+
233+
return html
234+
235+
def main():
236+
if len(sys.argv) < 2:
237+
print("Usage: python convert_my_results.py <json_file>")
238+
print("\nExample: python convert_my_results.py job_search_20251031.json")
239+
sys.exit(1)
240+
241+
json_file = sys.argv[1]
242+
243+
if not Path(json_file).exists():
244+
print(f"❌ File not found: {json_file}")
245+
sys.exit(1)
246+
247+
print(f"📁 Reading: {json_file}")
248+
249+
try:
250+
with open(json_file, 'r', encoding='utf-8') as f:
251+
data = json.load(f)
252+
except Exception as e:
253+
print(f"❌ Error reading JSON: {e}")
254+
sys.exit(1)
255+
256+
print("🎨 Generating beautiful HTML report...")
257+
258+
html = create_html_report(data)
259+
260+
output_file = "my_job_search_report.html"
261+
with open(output_file, 'w', encoding='utf-8') as f:
262+
f.write(html)
263+
264+
print(f"✅ Report generated: {output_file}")
265+
print(f" Double-click the file to open in your browser!")
266+
267+
# Try to open automatically
268+
import webbrowser
269+
try:
270+
abs_path = Path(output_file).absolute()
271+
webbrowser.open(f"file:///{abs_path}")
272+
print(f"🌐 Opening in browser...")
273+
except:
274+
print(f" Or open manually in your browser")
275+
276+
if __name__ == "__main__":
277+
main()
278+

generate_html_report.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate a beautiful HTML report from job search results JSON
4+
"""
5+
import json
6+
import sys
7+
from pathlib import Path
8+
9+
def generate_html_report(json_file: str = None, output_file: str = None):
10+
"""Generate HTML report from JSON results"""
11+
12+
# Find most recent results if not specified
13+
if json_file is None:
14+
results_dir = Path("job_search_results")
15+
if not results_dir.exists():
16+
print("❌ No job search results directory found")
17+
return False
18+
19+
json_files = sorted(
20+
results_dir.glob("job_search_*.json"),
21+
key=lambda x: x.stat().st_mtime,
22+
reverse=True
23+
)
24+
25+
if not json_files:
26+
print("❌ No job search results found")
27+
return False
28+
29+
json_file = str(json_files[0])
30+
print(f"📁 Using results file: {Path(json_file).name}")
31+
32+
# Load JSON data
33+
try:
34+
with open(json_file, 'r', encoding='utf-8') as f:
35+
data = json.load(f)
36+
except Exception as e:
37+
print(f"❌ Error reading JSON file: {e}")
38+
return False
39+
40+
# Read HTML template
41+
template_file = Path(__file__).parent / "view_results.html"
42+
if not template_file.exists():
43+
print(f"❌ Template file not found: {template_file}")
44+
return False
45+
46+
with open(template_file, 'r', encoding='utf-8') as f:
47+
html_content = f.read()
48+
49+
# Embed JSON data into HTML
50+
json_str = json.dumps(data, indent=2)
51+
html_content = html_content.replace('JSON_DATA_PLACEHOLDER', json_str)
52+
53+
# Determine output filename
54+
if output_file is None:
55+
output_file = "job_search_report.html"
56+
57+
# Write output
58+
with open(output_file, 'w', encoding='utf-8') as f:
59+
f.write(html_content)
60+
61+
print(f"✅ HTML report generated: {output_file}")
62+
print(f" Open in your browser to view")
63+
64+
# Try to open in browser
65+
import webbrowser
66+
try:
67+
webbrowser.open(f"file://{Path(output_file).absolute()}")
68+
print(f"🌐 Opening in browser...")
69+
except:
70+
pass
71+
72+
return True
73+
74+
if __name__ == "__main__":
75+
if len(sys.argv) > 1:
76+
json_file = sys.argv[1]
77+
output_file = sys.argv[2] if len(sys.argv) > 2 else "job_search_report.html"
78+
generate_html_report(json_file, output_file)
79+
else:
80+
generate_html_report()
81+

0 commit comments

Comments
 (0)