import gradio as gr import requests import json import os from dotenv import load_dotenv import time # Load environment variables from .env file (for local development) load_dotenv() # Get environment variables and strip whitespace organisationNumber = os.getenv("organisationNumber", "").strip() token = os.getenv("token", "").strip() postApiUrl = os.getenv("postApiUrl", "").strip() # RESTORED: ServiceTrigger environment variable ServiceTrigger = os.getenv("ServiceTrigger", "").strip() # Validate environment variables on startup missing_vars = [] if not organisationNumber: missing_vars.append("organisationNumber") if not token: missing_vars.append("token") if not postApiUrl: missing_vars.append("postApiUrl") # Added validation for ServiceTrigger if not ServiceTrigger: missing_vars.append("ServiceTrigger") if missing_vars: print(f"⚠️ WARNING: Missing environment variables: {', '.join(missing_vars)}") print("Please check your Hugging Face Spaces Secrets configuration") def notify_discord(name, job_post, post_url): # This URL should be loaded from your environment variables (Discord Webhook URL) discord_webhook_url = os.getenv("DISCORD_WEBHOOK_URL") if not discord_webhook_url: print("⚠️ WARNING: DISCORD_WEBHOOK_URL is not set. Skipping Discord notification.") return discord_message = { "content": "✨ New Job Posted! ✨", "embeds": [ { "title": f"Job Post by {name}", "description": job_post[:2048] + ('...' if len(job_post) > 2048 else ''), "url": post_url, "color": 16744704 # Orange color code } ] } response = requests.post(discord_webhook_url, json=discord_message) name = "abcd" job_post = "efgh" post_url = "https://www.linkedin.com/feed/update/urn:li:share:1234567890123456789/" discord_message = { "content": "✨ New Job Posted! ✨", "embeds": [ { "title": f"Job Post by {name}", "description": job_post[:2048] + ('...' if len(job_post) > 2048 else ''), "url": post_url, "color": 16744704 # Orange color code } ] } # notify_discord(name, job_post, post_url) def check_website_status(): """Check if the FeedHire website is accessible""" try: response = requests.get("https://feedhire.me/", timeout=10) if response.status_code == 200: return "🟢", "Online", "#10b981" else: return "🟡", f"Status {response.status_code}", "#f59e0b" except requests.exceptions.RequestException: return "🔴", "Offline", "#ef4444" def check_api_status(): """Check if the LinkedIn API is accessible and token is valid""" if not token or not organisationNumber: return "🔴", "No Credentials", "#ef4444" try: headers = { "Authorization": f"Bearer {token}", "LinkedIn-Version": "202510", "X-Restli-Protocol-Version": "2.0.0" } response = requests.get( f"https://api.linkedin.com/rest/organizations/{organisationNumber}", headers=headers, timeout=10 ) if response.status_code == 200: return "🟢", "Connected", "#10b981" elif response.status_code == 401: return "🔴", "Auth Failed", "#ef4444" elif response.status_code == 403: return "🟡", "Access Denied", "#f59e0b" else: return "🟡", f"Error {response.status_code}", "#f59e0b" except requests.exceptions.RequestException: return "🔴", "Connection Failed", "#ef4444" def get_status_display(): """Get formatted status display for both services""" web_icon, web_status, web_color = check_website_status() api_icon, api_status, api_color = check_api_status() html = f"""
{web_icon}
Website
{web_status}
{api_icon}
API
{api_status}
""" return html def post_to_linkedin(name, job_post): """Post to LinkedIn via their API with improved UX feedback""" yield "Verifying inputs...", gr.update(value="", visible=False) if not organisationNumber or not token or not postApiUrl: yield f"❌ Configuration Error: Missing LinkedIn API variables. Please check Spaces Secrets.", gr.update(visible=False) return # Check for ServiceTrigger (if critical for the process) if not ServiceTrigger: yield f"❌ Configuration Error: Missing ServiceTrigger variable. FeedHire update will be manual.", gr.update(visible=False) # Note: We continue execution here as the LinkedIn post is the primary goal if not name.strip() or not job_post.strip(): yield "❌ Please fill in all fields (Name and Job Post).", gr.update(visible=False) return headers_dict = { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json', 'X-Restli-Protocol-Version': '2.0.0', 'LinkedIn-Version': '202510' } data = { "author": f"urn:li:organization:{organisationNumber}", "commentary": f"{name}\n\n{job_post}", "visibility": "PUBLIC", "lifecycleState": "PUBLISHED", "distribution": { "feedDistribution": "MAIN_FEED", "targetEntities": [], "thirdPartyDistributionChannels": [] }, "isReshareDisabledByAuthor": False } try: yield "🚀 Posting to LinkedIn...", gr.update(visible=False) response = requests.post( postApiUrl, headers=headers_dict, json=data, timeout=30 ) if response.status_code == 201: for i in range(10, 0, -1): yield f"✅ Post successful! Waiting for LinkedIn API update... ({i} seconds remaining)", gr.update(visible=False) time.sleep(1) # RESTORED: Service Trigger Logic if ServiceTrigger: try: trigger_resp = requests.post(f"{ServiceTrigger}", timeout=10) yield "🔄 FeedHire website triggered, waiting for job listing update...", gr.update(visible=False) if trigger_resp.status_code == 200: yield "✅ Backend triggered successfully! FeedHire will update the listing shortly.", gr.update(visible=False) else: yield f"⚠️ Backend trigger failed (Status: {trigger_resp.status_code}). FeedHire update may be delayed.", gr.update(visible=False) except Exception as trigger_error: print(f"Error triggering async fetch: {trigger_error}") yield f"⚠️ Error triggering FeedHire backend: {trigger_error}", gr.update(visible=False) yield "✅ Post live! 🔍 Retrieving your post link...", gr.update(visible=False) try: post_api_url = "https://api.linkedin.com/rest/posts" headers = { "Authorization": f"Bearer {token}", "LinkedIn-Version": "202510", "X-Restli-Protocol-Version": "2.0.0" } params = { "q": "author", "author": f"urn:li:organization:{organisationNumber}", "count": 1 } fetch_response = requests.get(post_api_url, headers=headers, params=params, timeout=30) if fetch_response.status_code == 200: fetch_data = fetch_response.json() if fetch_data.get("elements") and len(fetch_data["elements"]) > 0: post_id_urn = fetch_data["elements"][0]["id"] if "share" in post_id_urn: post_id = post_id_urn.split(":")[-1] post_url = f"https://www.linkedin.com/feed/update/urn:li:share:{post_id}/" elif "activity" in post_id_urn: post_id = post_id_urn.split(":")[-1] post_url = f"https://www.linkedin.com/feed/update/urn:li:activity:{post_id}/" else: raise Exception("Unknown post URN format") print(f"Post URL: {post_url}") notify_discord(name, job_post, post_url) yield "✅ Post published successfully!", gr.update(value=f"### 🎉 [Click here to view your post on LinkedIn →]({post_url})", visible=True) return notify_discord(name, job_post, "https://www.linkedin.com/company/109539782/posts/") yield "✅ Post published! (Could not auto-retrieve post link. Please check the LinkedIn page.)", gr.update(visible=False) except Exception as fetch_error: print(f"Error fetching post: {fetch_error}") notify_discord(name, job_post, "https://www.linkedin.com/company/109539782/posts/") yield f"✅ Post published! (Error retrieving link: {fetch_error})", gr.update(visible=False) else: try: error_data = response.json() error_message = error_data.get('message', response.text) except requests.exceptions.JSONDecodeError: error_message = response.text error_msg = f"❌ Error {response.status_code}: {error_message}" print(error_msg) yield error_msg, gr.update(visible=False) except requests.exceptions.Timeout: yield "❌ Error: Request timed out. Please try again.", gr.update(visible=False) except requests.exceptions.RequestException as e: yield f"❌ Network Error: {str(e)}", gr.update(visible=False) except Exception as e: yield f"❌ Error: {str(e)}", gr.update(visible=False) # --- Ultra-Premium Animated Gradio Interface with Orange Theme --- custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); * { font-family: 'Inter', sans-serif !important; } .gradio-container { max-width: 1400px !important; margin: auto !important; } .transparent-box { background: transparent !important; border: none !important; /* Remove border */ box-shadow: none !important; /* Remove shadow */ padding: 0 !important; /* Optional: remove extra padding */ } /* Transparent container + all children */ .transparent-box, .transparent-box > div, .transparent-box * { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; } /* Hero Section Animations (Unchanged) */ @keyframes fadeInDown { from { opacity: 0; transform: translateY(-30px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } @keyframes scaleIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } /* Updated Glow to Orange */ @keyframes glow { 0%, 100% { box-shadow: 0 0 20px rgba(255, 126, 0, 0.5); /* Vibrant Orange glow */ } 50% { box-shadow: 0 0 40px rgba(255, 126, 0, 0.8); /* Vibrant Orange glow */ } } @keyframes slideInLeft { from { opacity: 0; transform: translateX(-50px); } to { opacity: 1; transform: translateX(0); } } @keyframes slideInRight { from { opacity: 0; transform: translateX(50px); } to { opacity: 1; transform: translateX(0); } } /* Hero Section - Updated Gradient & Shadow */ /* Make all inner Gradio containers fully transparent */ #hero-section, #hero-section > div, #hero-section > div > div { background: transparent !important; box-shadow: none !important; padding: 0 !important; margin: 0 !important; } .hero-column > div { background: transparent !important; padding: 0 !important; margin: 0 !important; } .logo-container { animation: scaleIn 1s ease-out 0.3s both; display: flex; justify-content: center; align-items: center; background: rgba(255,255,255,0.15); border-radius: 20px; padding: 15px; backdrop-filter: blur(10px); border: 2px solid rgba(255,255,255,0.3); transition: all 0.4s ease; } .logo-container:hover { transform: scale(1.05) rotate(2deg); box-shadow: 0 10px 30px rgba(0,0,0,0.3); } .hero-title { animation: fadeInUp 1s ease-out 0.5s both; } .hero-subtitle { animation: fadeInUp 1s ease-out 0.7s both; } .status-container { display: flex; gap: 15px; justify-content: flex-end; align-items: center; padding: 12px 20px; background: rgba(15,23,42,0.7); border-radius: 12px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.15); animation: fadeInRight 1s ease-out 0.9s both; transition: all 0.3s ease; } .status-container:hover { background: rgba(15,23,42,0.85); transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.3); } .status-item { display: flex; align-items: center; gap: 8px; } .status-icon { font-size: 20px; animation: pulse 2s ease-in-out infinite; } .status-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; } .status-value { font-size: 13px; font-weight: 700; } .status-divider { width: 1px; height: 30px; background: rgba(255,255,255,0.2); } /* Banner Animation */ .info-banner { animation: fadeInUp 1s ease-out 1.1s both; } /* Form Section Animations */ .form-section { animation: slideInLeft 0.8s ease-out 0.3s both; } #job-form { border: 2px solid #FF7E00; border-radius: 16px; padding: 25px; background: rgba(255,255,255,0.05); box-shadow: 0 10px 30px rgba(255,126,0,0.2); } .form-section { animation: slideInLeft 0.8s ease-out 0.3s both; border: 2px solid #FF7E00; /* Orange border */ border-radius: 16px; padding: 25px; background: rgba(255,255,255,0.05); /* optional translucent bg */ box-shadow: 0 10px 30px rgba(255, 126, 0, 0.2); /* subtle orange shadow */ } .benefits-section { animation: slideInRight 0.8s ease-out 0.3s both; } /* Input Focus Effects - Updated Border & Shadow */ textarea:focus, input:focus { outline: none !important; border: 2px solid #FF7E00 !important; /* Vibrant Orange border */ box-shadow: 0 0 0 3px rgba(255, 126, 0, 0.1), 0 0 20px rgba(255, 126, 0, 0.2) !important; /* Orange focus glow */ transition: all 0.3s ease !important; } /* Button Animations - Updated Gradient, Hover, and Shadow */ button[variant="primary"] { background: linear-gradient(135deg, #FF7E00 0%, #FF9A3C 100%) !important; /* Orange-Gold Gradient */ border: none !important; font-weight: 700 !important; transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; position: relative !important; overflow: hidden !important; } button[variant="primary"]::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: rgba(255,255,255,0.3); transform: translate(-50%, -50%); transition: width 0.6s, height 0.6s; } button[variant="primary"]:hover::before { width: 300px; height: 300px; } button[variant="primary"]:hover { transform: translateY(-4px) scale(1.02) !important; box-shadow: 0 15px 40px rgba(255, 126, 0, 0.5) !important; /* Orange shadow on hover */ } button[variant="primary"]:active { transform: translateY(-2px) scale(0.98) !important; } .refresh-btn { font-size: 12px !important; padding: 6px 14px !important; background: transparent !important; /* Updated to the requested orange border */ border: 1px solid #FF7E00 !important; color: white !important; border-radius: 8px !important; transition: all 0.3s ease !important; font-weight: 600 !important; } .refresh-btn:hover { background: linear-gradient(135deg, rgba(255, 126, 0, 0.3), rgba(255, 154, 60, 0.4)) !important; box-shadow: 0 0 15px rgba(255, 126, 0, 0.8), 0 0 25px rgba(255, 154, 60, 0.6); transition: all 0.5s ease; color: #FF7E00 !important; /* ensures icon/text matches theme */ } /* Card Animations - Updated Border Color */ .benefit-card { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 12px; border-left: 4px solid #FF7E00; /* Vibrant Orange border */ transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); opacity: 0; transform: translateY(20px); } .benefit-card.animate { animation: fadeInUp 0.6s ease-out forwards; } .benefit-card:nth-child(1) { animation-delay: 0.1s; } .benefit-card:nth-child(2) { animation-delay: 0.2s; } .benefit-card:nth-child(3) { animation-delay: 0.3s; } .benefit-card:nth-child(4) { animation-delay: 0.4s; } .benefit-card:hover { background: rgba(255,255,255,0.1); transform: translateX(10px) translateY(-5px); box-shadow: 0 10px 30px rgba(255, 126, 0, 0.3); /* Orange shadow on hover */ border-left-width: 6px; } /* How It Works Cards */ .how-it-works-card { background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 16px; padding: 30px; border: 2px solid rgba(255,255,255,0.2); transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); opacity: 0; transform: scale(0.8); } .how-it-works-card.animate { animation: scaleIn 0.6s ease-out forwards; } .how-it-works-card:nth-child(1) { animation-delay: 0.2s; } .how-it-works-card:nth-child(2) { animation-delay: 0.4s; } .how-it-works-card:nth-child(3) { animation-delay: 0.6s; } .how-it-works-card:hover { transform: translateY(-10px) scale(1.05); box-shadow: 0 20px 50px rgba(0,0,0,0.3); border-color: rgba(255,255,255,0.4); } .how-it-works-card .emoji { display: inline-block; transition: transform 0.3s ease; } .how-it-works-card:hover .emoji { transform: scale(1.2) rotate(10deg); } /* Success Link Animation - Updated Gradient & Glow */ #success-link-box { text-align: center; padding: 1.5rem; background: linear-gradient(135deg, #FF7E00 0%, #FF9A3C 100%); /* Orange-Gold Gradient */ border-radius: 12px; box-shadow: 0 10px 30px rgba(255, 126, 0, 0.3); /* Orange shadow */ animation: slideInUp 0.5s ease-out, glow 2s ease-in-out infinite; } @keyframes slideInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } #success-link-box a { font-size: 1.2rem; font-weight: 700; color: white !important; text-decoration: none; transition: all 0.3s ease; } #success-link-box a:hover { text-decoration: underline; transform: scale(1.05); display: inline-block; } /* Benefits Box */ #benefits-box { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); padding: 35px; border-radius: 20px; height: 100%; display: flex; flex-direction: column; gap: 25px; box-shadow: 0 25px 70px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.1); position: relative; overflow: hidden; } /* Benefits Box ::before - Updated Radial Gradient Color */ #benefits-box::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle at top right, rgba(255, 126, 0, 0.1) 0%, transparent 70%); /* Orange tint */ pointer-events: none; } /* Responsive adjustments */ @media (max-width: 768px) { #hero-section { padding: 2rem 1.5rem; } .status-container { flex-direction: column; gap: 10px; } .status-divider { width: 100%; height: 1px; } } """ with gr.Blocks(theme=gr.themes.Soft(primary_hue="orange", secondary_hue="yellow"), css=custom_css) as demo: # Hero Section with Animated Logo and Status with gr.Group(elem_id="hero-section"): with gr.Row(): with gr.Column(scale=20, elem_classes="hero-column"): gr.HTML("""

FeedHire Job Poster

Post jobs instantly to LinkedIn & reach thousands of candidates 🚀

""") with gr.Column(scale=4, min_width=280): status_display = gr.HTML(get_status_display()) refresh_status_btn = gr.Button("↺ Refresh", size="sm", elem_classes="refresh-btn") # Visibility Info Banner - Updated Background and Border gr.HTML("""

🌐 Your job post will be live on LinkedIn and visible on FeedHire.me within minutes ⚡

""") if missing_vars: gr.Warning(f"⚠️ Missing configuration: {', '.join(missing_vars)}") # Main Content: Two-Column Layout with gr.Row(equal_height=True): # LEFT COLUMN - Job Posting Form with gr.Column(scale=1, elem_classes="form-section transparent-box, ", elem_id="job-form"): gr.HTML("

📝 Create Your Job Post

") name_input = gr.Textbox( label="👤 Your Name", placeholder="e.g., Arif Raafi", lines=1 ) job_post_input = gr.Textbox( label="💼 Job Post Content", placeholder="✨ Craft your perfect job post here...\n\n🎯 Role: \n📋 Responsibilities: \n📍 Location: \n💰 Salary Range: \n📧 How to Apply: ", lines=12 ) submit_btn = gr.Button("🚀 Publish to LinkedIn", variant="primary", size="lg") output = gr.Textbox(label="📊 Status", lines=2, interactive=False) success_link = gr.Markdown( "", visible=False, elem_id="success-link-box" ) # RIGHT COLUMN - Benefits & Marketing with gr.Column(scale=1, elem_id="job-form", elem_classes="benefits-section transparent-box"): gr.HTML("""

🌟 Why Choose FeedHire?

""") # The image component was missing in the previous step's final output, but present in the context. Restoring it. gr.Image( value="analytics_map.png", show_label=False, show_download_button=False, container=False, elem_classes="benefits-image", ) gr.HTML("""
🌍
Massive Reach - Tap into LinkedIn's network and FeedHire's global ecosystem
Lightning Fast - Post once, go live instantly on multiple platforms
💯
100% Free - No hidden fees, no subscriptions—just pure visibility
🎯
Quality Candidates - Attract serious applicants
""") # How It Works Section - Updated Gradient gr.HTML("""

✨ How FeedHire Works

📝

Create

Fill out the form with your name and job details—make it compelling!

🚀

Publish

Hit publish and watch it go live on LinkedIn instantly via API

Go Live

Our AI refines your post before publishing it live on FeedHire.me—start attracting applicants immediately!

""") # Footer Disclaimer gr.HTML("""

⚠️ Important: FeedHire is not responsible for job post content. Ensure all submissions are accurate and lawful. Fake or misleading posts will be removed.

""") # Click Actions (Unchanged) submit_btn.click( fn=post_to_linkedin, inputs=[name_input, job_post_input], outputs=[output, success_link], show_progress="full" ) def refresh_status(): return get_status_display() refresh_status_btn.click( fn=refresh_status, outputs=[status_display] ) demo.load( fn=refresh_status, outputs=[status_display] ) if __name__ == "__main__": demo.launch(allowed_paths=["."])