| from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_from_directory |
| from flask_socketio import SocketIO, emit |
| from werkzeug.utils import secure_filename |
| import os, json, sqlite3, bcrypt, re, smtplib |
| from email.message import EmailMessage |
| from datetime import datetime |
| from functools import wraps |
| from skill_questions import extract_skills_from_text, get_question_for_skill, score_answer, SKILL_QUESTIONS, start_interview, continue_interview, reset_interview, INTERVIEW_SESSIONS |
| from ai_pipeline import extract_skills_ai, extract_full_resume, generate_question_ai, score_answer_ai, generate_signed_url, verify_signed_url |
|
|
| app = Flask(__name__) |
| app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-prod') |
| app.config['UPLOAD_FOLDER'] = os.environ.get('UPLOAD_FOLDER', 'uploads') |
| app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 |
| app.config['SESSION_PERMANENT'] = False |
|
|
| |
| app.config['SMTP_SERVER'] = os.environ.get('SMTP_SERVER', '') |
| app.config['SMTP_PORT'] = int(os.environ.get('SMTP_PORT', '587')) |
| app.config['SMTP_USERNAME'] = os.environ.get('SMTP_USERNAME', '') |
| app.config['SMTP_PASSWORD'] = os.environ.get('SMTP_PASSWORD', '') |
| app.config['MAIL_FROM'] = os.environ.get('MAIL_FROM', 'noreply@nexusats.com') |
|
|
| socketio = SocketIO(app, cors_allowed_origins="*") |
| os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) |
|
|
| @app.errorhandler(404) |
| def handle_404(e): |
| if request.path.startswith('/api/'): |
| return jsonify({'success': False, 'message': 'Endpoint not found'}), 404 |
| return render_template('index.html'), 404 |
|
|
| @app.errorhandler(500) |
| def handle_500(e): |
| return jsonify({'success': False, 'message': 'Internal server error'}), 500 |
|
|
| DB_PATH = os.environ.get('DB_PATH', 'nexus_ats.db') |
|
|
| def _safe_int(val, default=0): |
| try: |
| return int(val) |
| except (ValueError, TypeError): |
| return default |
|
|
| def get_db(): |
| conn = sqlite3.connect(DB_PATH) |
| conn.row_factory = sqlite3.Row |
| conn.execute("PRAGMA journal_mode=WAL") |
| conn.execute("PRAGMA foreign_keys=ON") |
| return conn |
|
|
| def init_db(): |
| conn = get_db() |
| conn.executescript(""" |
| CREATE TABLE IF NOT EXISTS users ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| username TEXT UNIQUE NOT NULL, |
| email TEXT UNIQUE NOT NULL, |
| password_hash TEXT NOT NULL, |
| role TEXT DEFAULT 'seeker', |
| company TEXT DEFAULT '', |
| verified INTEGER DEFAULT 0, |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| CREATE TABLE IF NOT EXISTS resumes ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| filename TEXT, |
| extracted_text TEXT, |
| skills TEXT, |
| experience TEXT, |
| education TEXT, |
| certifications TEXT, |
| uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (user_id) REFERENCES users(id) |
| ); |
| CREATE TABLE IF NOT EXISTS jobs ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| hirer_id INTEGER NOT NULL, |
| company TEXT DEFAULT '', |
| title TEXT NOT NULL, |
| description TEXT DEFAULT '', |
| location TEXT DEFAULT '', |
| employment_type TEXT DEFAULT 'Full-time', |
| salary_min INTEGER DEFAULT 0, |
| salary_max INTEGER DEFAULT 0, |
| required_skills TEXT DEFAULT '[]', |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| active INTEGER DEFAULT 1, |
| FOREIGN KEY (hirer_id) REFERENCES users(id) |
| ); |
| CREATE TABLE IF NOT EXISTS applications ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| job_id INTEGER NOT NULL, |
| seeker_id INTEGER NOT NULL, |
| seeker_name TEXT DEFAULT '', |
| seeker_email TEXT DEFAULT '', |
| resume_text TEXT DEFAULT '', |
| resume_filename TEXT DEFAULT '', |
| skills TEXT DEFAULT '[]', |
| match_score REAL DEFAULT 0, |
| status TEXT DEFAULT 'applied', |
| applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (job_id) REFERENCES jobs(id), |
| FOREIGN KEY (seeker_id) REFERENCES users(id) |
| ); |
| CREATE TABLE IF NOT EXISTS trending_skills ( |
| skill TEXT PRIMARY KEY, |
| frequency INTEGER DEFAULT 0, |
| avg_salary REAL DEFAULT 0, |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
| ); |
| CREATE TABLE IF NOT EXISTS sent_emails ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| from_user_id INTEGER NOT NULL, |
| recipient TEXT NOT NULL, |
| subject TEXT DEFAULT '', |
| sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (from_user_id) REFERENCES users(id) |
| ); |
| CREATE TABLE IF NOT EXISTS profiles ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER UNIQUE NOT NULL, |
| phone TEXT DEFAULT '', |
| location TEXT DEFAULT '', |
| headline TEXT DEFAULT '', |
| summary TEXT DEFAULT '', |
| experience TEXT DEFAULT '[]', |
| education TEXT DEFAULT '[]', |
| certifications TEXT DEFAULT '[]', |
| linkedin_url TEXT DEFAULT '', |
| github_url TEXT DEFAULT '', |
| portfolio_url TEXT DEFAULT '', |
| years_of_experience INTEGER DEFAULT 0, |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (user_id) REFERENCES users(id) |
| ); |
| """) |
| conn.commit() |
| conn.close() |
|
|
| init_db() |
|
|
| |
| def migrate_db(): |
| conn = get_db() |
| try: |
| conn.execute("SELECT resume_filename FROM applications LIMIT 1") |
| except sqlite3.OperationalError: |
| conn.execute("ALTER TABLE applications ADD COLUMN resume_filename TEXT DEFAULT ''") |
| conn.commit() |
| print(" [db] Added resume_filename column to applications") |
| try: |
| conn.execute("SELECT resume_filename FROM applications LIMIT 1") |
| except sqlite3.OperationalError: |
| pass |
| |
| for col, col_type in [('company_logo', 'TEXT DEFAULT ""'), ('company_desc', 'TEXT DEFAULT ""'), ('company_size', 'TEXT DEFAULT ""'), ('company_website', 'TEXT DEFAULT ""')]: |
| try: |
| conn.execute(f"ALTER TABLE users ADD COLUMN {col} {col_type}") |
| except sqlite3.OperationalError: |
| pass |
| for col, col_type in [('job_views', 'INTEGER DEFAULT 0')]: |
| try: |
| conn.execute(f"ALTER TABLE jobs ADD COLUMN {col} {col_type}") |
| except sqlite3.OperationalError: |
| pass |
| for col, col_type in [('reported', 'INTEGER DEFAULT 0'), ('report_reason', 'TEXT DEFAULT ""')]: |
| try: |
| conn.execute(f"ALTER TABLE jobs ADD COLUMN {col} {col_type}") |
| except sqlite3.OperationalError: |
| pass |
| |
| try: |
| conn.execute("SELECT phone FROM profiles LIMIT 1") |
| except sqlite3.OperationalError: |
| conn.executescript(""" |
| CREATE TABLE IF NOT EXISTS profiles ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER UNIQUE NOT NULL, |
| phone TEXT DEFAULT '', |
| location TEXT DEFAULT '', |
| headline TEXT DEFAULT '', |
| summary TEXT DEFAULT '', |
| experience TEXT DEFAULT '[]', |
| education TEXT DEFAULT '[]', |
| certifications TEXT DEFAULT '[]', |
| linkedin_url TEXT DEFAULT '', |
| github_url TEXT DEFAULT '', |
| portfolio_url TEXT DEFAULT '', |
| years_of_experience INTEGER DEFAULT 0, |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (user_id) REFERENCES users(id) |
| ); |
| """) |
| conn.commit() |
| print(" [db] Created profiles table") |
| conn.close() |
| migrate_db() |
|
|
| |
| def scan_trending_skills(): |
| """Scan all active jobs and compute skill frequency & average salary.""" |
| conn = get_db() |
| jobs = conn.execute("SELECT description, required_skills, salary_min, salary_max FROM jobs WHERE active=1").fetchall() |
| skill_data = {} |
| for job in jobs: |
| text = (job['description'] or '') + ' ' + ' '.join(json.loads(job['required_skills'] or '[]')) |
| avg_sal = ((job['salary_min'] or 0) + (job['salary_max'] or 0)) / 2 |
| for skill in extract_skills_from_text(text): |
| if skill not in skill_data: |
| skill_data[skill] = {'frequency': 0, 'salaries': []} |
| skill_data[skill]['frequency'] += 1 |
| if avg_sal > 0: |
| skill_data[skill]['salaries'].append(avg_sal) |
| |
| conn.execute("DELETE FROM trending_skills") |
| for skill, data in sorted(skill_data.items(), key=lambda x: -x[1]['frequency'])[:15]: |
| avg_sal = sum(data['salaries']) / max(len(data['salaries']), 1) if data['salaries'] else 0 |
| conn.execute("INSERT INTO trending_skills (skill, frequency, avg_salary) VALUES (?,?,?)", |
| (skill, data['frequency'], round(avg_sal))) |
| conn.commit() |
| conn.close() |
| print(f" [trending] Scanned {len(jobs)} jobs, computed {len(skill_data)} trending skills") |
|
|
| |
| def seed_users(): |
| conn = get_db() |
| count = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()['c'] |
| if count == 0: |
| pw = bcrypt.hashpw('password123'.encode(), bcrypt.gensalt()).decode() |
| admin_pw = bcrypt.hashpw('admin123'.encode(), bcrypt.gensalt()).decode() |
| conn.execute("INSERT INTO users (username, email, password_hash, role, company, verified) VALUES (?,?,?,?,?,?)", |
| ('Nexus Admin', 'admin@test.com', admin_pw, 'admin', 'Nexus ATS', 1)) |
| conn.execute("INSERT INTO users (username, email, password_hash, role, company, verified) VALUES (?,?,?,?,?,?)", |
| ('Alex Johnson', 'seeker@test.com', pw, 'seeker', '', 1)) |
| conn.execute("INSERT INTO users (username, email, password_hash, role, company, verified) VALUES (?,?,?,?,?,?)", |
| ('TechCorp HR', 'hirer@test.com', pw, 'hirer', 'TechCorp', 0)) |
| conn.commit() |
| conn.close() |
| seed_users() |
|
|
| |
| def seed_jobs(): |
| conn = get_db() |
| count = conn.execute("SELECT COUNT(*) as c FROM jobs").fetchone()['c'] |
| if count == 0: |
| hirer = conn.execute("SELECT id FROM users WHERE role='hirer' LIMIT 1").fetchone() |
| if hirer: |
| jobs = [ |
| ('Senior Python Developer', 'TechCorp', 'New York, NY', 'Build and maintain backend services. 5+ years Python experience required.', 120000, 150000, json.dumps(['Python','Django','AWS','PostgreSQL'])), |
| ('Frontend Engineer', 'WebSolutions', 'Remote', 'Build modern React applications. Experience with TypeScript required.', 100000, 130000, json.dumps(['React','TypeScript','Next.js','Tailwind'])), |
| ('Data Scientist', 'DataPro', 'San Francisco, CA', 'Apply ML to business problems. Strong Python and TensorFlow skills needed.', 140000, 180000, json.dumps(['Python','TensorFlow','SQL','Machine Learning'])), |
| ('DevOps Engineer', 'CloudScale', 'Austin, TX', 'Manage cloud infrastructure. Docker and Kubernetes expertise required.', 110000, 145000, json.dumps(['Docker','Kubernetes','AWS','Terraform'])), |
| ] |
| for j in jobs: |
| conn.execute("INSERT INTO jobs (hirer_id, company, title, description, location, salary_min, salary_max, required_skills) VALUES (?,?,?,?,?,?,?,?)", |
| (hirer['id'], *j)) |
| conn.commit() |
| conn.close() |
| seed_jobs() |
|
|
| |
| try: |
| scan_trending_skills() |
| except Exception as e: |
| print(f" [trending] Scan fallback: {e}") |
| conn = get_db() |
| skills = [('Python', 120, 125000), ('React', 95, 115000), ('AWS', 80, 130000), ('TypeScript', 75, 120000), ('Node.js', 70, 110000), ('Docker', 65, 125000)] |
| for s,f,a in skills: |
| conn.execute("INSERT OR IGNORE INTO trending_skills (skill, frequency, avg_salary) VALUES (?,?,?)", (s,f,a)) |
| conn.commit() |
| conn.close() |
|
|
| |
| def get_current_user(): |
| user_id = session.get('user_id') |
| if not user_id: |
| return None |
| conn = get_db() |
| user = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone() |
| conn.close() |
| return dict(user) if user else None |
|
|
| def login_required(f): |
| @wraps(f) |
| def wrapper(*a, **kw): |
| if not session.get('user_id'): |
| return jsonify({'success': False, 'message': 'Not logged in'}), 401 |
| return f(*a, **kw) |
| return wrapper |
|
|
| |
| @app.context_processor |
| def inject_user(): |
| return {'current_user': get_current_user()} |
|
|
| |
| @app.route('/') |
| def index(): |
| return render_template('index.html') |
|
|
| @app.route('/login') |
| def login_page(): |
| return render_template('login.html') |
|
|
| @app.route('/register') |
| def register_page(): |
| return render_template('register.html') |
|
|
| @app.route('/dashboard/seeker') |
| def seeker_dashboard(): |
| if not session.get('user_id'): |
| return redirect(url_for('login_page')) |
| return render_template('seeker_dashboard.html') |
|
|
| @app.route('/dashboard/hirer') |
| def hirer_dashboard(): |
| if not session.get('user_id'): |
| return redirect(url_for('login_page')) |
| return render_template('hirer_dashboard.html') |
|
|
| @app.route('/admin') |
| def admin_dashboard(): |
| if not session.get('user_id'): |
| return redirect(url_for('login_page')) |
| return render_template('admin.html') |
|
|
| @app.route('/logout') |
| def logout(): |
| session.clear() |
| return redirect(url_for('index')) |
|
|
| |
| @app.route('/api/register', methods=['POST']) |
| def register(): |
| data = request.json or {} |
| username = data.get('username') |
| email = data.get('email') |
| password = data.get('password') |
| role = data.get('role', 'seeker') |
| company = data.get('company', '') |
| if not username or not email or not password: |
| return jsonify({'success': False, 'message': 'Missing required fields'}), 400 |
| if len(password) < 8: |
| return jsonify({'success': False, 'message': 'Password too short (min 8 chars)'}), 400 |
| pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() |
| conn = get_db() |
| try: |
| conn.execute("INSERT INTO users (username, email, password_hash, role, company) VALUES (?,?,?,?,?)", |
| (username, email, pw_hash, role, company)) |
| conn.commit() |
| user = conn.execute("SELECT * FROM users WHERE email=?", (email,)).fetchone() |
| session['user_id'] = user['id'] |
| return jsonify({'success': True, 'user': dict(user)}) |
| except sqlite3.IntegrityError: |
| return jsonify({'success': False, 'message': 'Email or username already exists'}), 409 |
| finally: |
| conn.close() |
|
|
| @app.route('/api/login', methods=['POST']) |
| def login(): |
| data = request.json or {} |
| email = data.get('email') |
| password = data.get('password') |
| if not email or not password: |
| return jsonify({'success': False, 'message': 'Missing email or password'}), 400 |
| conn = get_db() |
| user = conn.execute("SELECT * FROM users WHERE email=?", (email,)).fetchone() |
| conn.close() |
| if not user or not bcrypt.checkpw(password.encode(), user['password_hash'].encode()): |
| return jsonify({'success': False, 'message': 'Invalid email or password'}), 401 |
| session['user_id'] = user['id'] |
| return jsonify({'success': True, 'user': dict(user)}) |
|
|
| @app.route('/api/me') |
| def api_me(): |
| user = get_current_user() |
| if not user: |
| return jsonify({'success': False}), 401 |
| return jsonify({'success': True, 'user': user}) |
|
|
| @app.route('/api/profile', methods=['GET', 'POST']) |
| @login_required |
| def api_profile(): |
| user = get_current_user() |
| conn = get_db() |
| |
| if request.method == 'POST': |
| data = request.json or {} |
| phone = data.get('phone', '') |
| location = data.get('location', '') |
| headline = data.get('headline', '') |
| summary = data.get('summary', '') |
| linkedin_url = data.get('linkedin_url', '') |
| github_url = data.get('github_url', '') |
| portfolio_url = data.get('portfolio_url', '') |
| experience = json.dumps(data.get('experience', [])) |
| education = json.dumps(data.get('education', [])) |
| certifications = json.dumps(data.get('certifications', [])) |
| years_of_experience = _safe_int(data.get('years_of_experience', 0)) |
| |
| conn.execute(""" |
| INSERT INTO profiles (user_id, phone, location, headline, summary, experience, education, |
| certifications, linkedin_url, github_url, portfolio_url, years_of_experience) |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |
| ON CONFLICT(user_id) DO UPDATE SET |
| phone=excluded.phone, location=excluded.location, headline=excluded.headline, |
| summary=excluded.summary, experience=excluded.experience, education=excluded.education, |
| certifications=excluded.certifications, linkedin_url=excluded.linkedin_url, |
| github_url=excluded.github_url, portfolio_url=excluded.portfolio_url, |
| years_of_experience=excluded.years_of_experience, updated_at=CURRENT_TIMESTAMP |
| """, (user['id'], phone, location, headline, summary, experience, education, |
| certifications, linkedin_url, github_url, portfolio_url, years_of_experience)) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True, 'message': 'Profile updated'}) |
| |
| |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| profile = conn.execute("SELECT * FROM profiles WHERE user_id=?", (user['id'],)).fetchone() |
| conn.close() |
| |
| data = dict(user) |
| if resume: |
| data['skills'] = json.loads(resume['skills'] or '[]') |
| data['resume_filename'] = resume['filename'] |
| if profile: |
| p = dict(profile) |
| for k, v in p.items(): |
| if k not in ('id', 'user_id', 'updated_at'): |
| try: |
| data[k] = json.loads(v) if isinstance(v, str) and v.startswith('[') else v |
| except: |
| data[k] = v |
| else: |
| data['phone'] = data.get('phone', '') |
| data['location'] = '' |
| data['headline'] = '' |
| data['summary'] = '' |
| data['experience'] = [] |
| data['education'] = [] |
| data['certifications'] = [] |
| data['linkedin_url'] = '' |
| data['github_url'] = '' |
| data['portfolio_url'] = '' |
| data['years_of_experience'] = 0 |
| |
| return jsonify({'success': True, 'profile': data}) |
|
|
| @app.route('/api/profile/<int:target_id>') |
| @login_required |
| def api_profile_view(target_id): |
| """View another user's public profile (for hirers to see seeker details).""" |
| conn = get_db() |
| target = conn.execute("SELECT id, username, email, role, company, verified FROM users WHERE id=?", (target_id,)).fetchone() |
| if not target: |
| conn.close() |
| return jsonify({'success': False, 'message': 'User not found'}), 404 |
| profile = conn.execute("SELECT * FROM profiles WHERE user_id=?", (target_id,)).fetchone() |
| resume = conn.execute("SELECT skills, filename FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (target_id,)).fetchone() |
| conn.close() |
| |
| data = dict(target) |
| if profile: |
| p = dict(profile) |
| for k in ('phone','location','headline','summary','experience','education','certifications', |
| 'linkedin_url','github_url','portfolio_url','years_of_experience'): |
| v = p.get(k, '') |
| try: |
| data[k] = json.loads(v) if isinstance(v, str) and v.startswith('[') else v |
| except: |
| data[k] = v |
| if resume: |
| data['skills'] = json.loads(resume['skills'] or '[]') |
| data['resume_filename'] = resume['filename'] |
| |
| return jsonify({'success': True, 'profile': data}) |
|
|
| @app.route('/api/verification-history') |
| @login_required |
| def verification_history(): |
| user = get_current_user() |
| conn = get_db() |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| conn.close() |
| skills = json.loads(resume['skills']) if resume and resume['skills'] else [] |
| result = [] |
| for s in skills: |
| clean = s.replace(' (Verified)', '') |
| verified = s.endswith('(Verified)') |
| result.append({'skill': clean, 'verified': verified}) |
| return jsonify({'success': True, 'skills': result}) |
|
|
| |
| @app.route('/api/upload-resume', methods=['POST']) |
| @login_required |
| def upload_resume(): |
| user = get_current_user() |
| if 'resume' not in request.files: |
| return jsonify({'success': False, 'message': 'No file'}), 400 |
| file = request.files['resume'] |
| if not file.filename or not file.filename.lower().endswith(('.pdf', '.docx')): |
| return jsonify({'success': False, 'message': 'Invalid type. PDF or DOCX only.'}), 400 |
| safe_name = secure_filename(file.filename) |
| if not safe_name: |
| safe_name = f"resume_{user['id']}.pdf" |
| path = os.path.join(app.config['UPLOAD_FOLDER'], f"{user['id']}_{safe_name}") |
| file.save(path) |
| |
| |
| text = "" |
| raw_bytes = b"" |
| file.seek(0) |
| raw_bytes = file.read() |
| try: |
| if file.filename.lower().endswith('.pdf'): |
| from pdfminer.high_level import extract_text as pdf_extract |
| text = pdf_extract(path) |
| else: |
| import docx2txt |
| text = docx2txt.process(path) |
| except Exception as e: |
| |
| try: |
| text = raw_bytes.decode('utf-8', errors='ignore') |
| except: |
| text = f"Resume of {user['username']}" |
| print(f" [resume] Parse warning for {file.filename}: {e}") |
| |
| |
| extracted = extract_full_resume(text) |
| extracted_skills = extracted.get('skills', []) |
| if not extracted_skills: |
| extracted_skills = extract_skills_from_text(text) |
| if not extracted_skills: |
| extracted_skills = extract_skills_from_text(safe_name.replace('_', ' ').replace('-', ' ')) |
| if not extracted_skills: |
| extracted_skills = ['Python', 'JavaScript', 'React', 'SQL'] |
| |
| conn = get_db() |
| conn.execute("DELETE FROM resumes WHERE user_id=?", (user['id'],)) |
| cur = conn.execute(""" |
| INSERT INTO resumes (user_id, filename, extracted_text, skills, experience, education, certifications) |
| VALUES (?,?,?,?,?,?,?) |
| """, (user['id'], safe_name, text[:5000], json.dumps(extracted_skills), |
| extracted.get('experience', '[]'), extracted.get('education', '[]'), |
| extracted.get('certifications', '[]'))) |
| resume_id = cur.lastrowid |
| |
| |
| conn.execute(""" |
| INSERT INTO profiles (user_id, phone, location, headline, summary, experience, education, |
| certifications, linkedin_url, github_url, portfolio_url, years_of_experience) |
| VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |
| ON CONFLICT(user_id) DO UPDATE SET |
| phone=excluded.phone, location=excluded.location, headline=excluded.headline, |
| summary=excluded.summary, experience=excluded.experience, education=excluded.education, |
| certifications=excluded.certifications, linkedin_url=excluded.linkedin_url, |
| github_url=excluded.github_url, portfolio_url=excluded.portfolio_url, |
| years_of_experience=excluded.years_of_experience, updated_at=CURRENT_TIMESTAMP |
| """, ( |
| user['id'], |
| extracted.get('phone', ''), |
| extracted.get('location', ''), |
| extracted.get('headline', ''), |
| extracted.get('summary', ''), |
| extracted.get('experience', '[]'), |
| extracted.get('education', '[]'), |
| extracted.get('certifications', '[]'), |
| extracted.get('urls', {}).get('linkedin', ''), |
| extracted.get('urls', {}).get('github', ''), |
| extracted.get('urls', {}).get('portfolio', ''), |
| extracted.get('years_of_experience', 0) |
| )) |
| conn.commit() |
| conn.close() |
| return jsonify({ |
| 'success': True, |
| 'resume_id': resume_id, |
| 'extracted_skills': extracted_skills, |
| 'skill_count': len(extracted_skills), |
| 'extracted': extracted |
| }) |
|
|
| @app.route('/api/get-verification-questions', methods=['POST']) |
| @login_required |
| def get_verification_questions(): |
| """Get AI verification questions for the user's extracted skills.""" |
| data = request.json or {} |
| skills = data.get('skills', []) |
| if not skills: |
| user = get_current_user() |
| conn = get_db() |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| conn.close() |
| if resume: |
| skills = json.loads(resume['skills'] or '[]') |
| |
| skills = [s.replace(' (Verified)', '') for s in skills if 'Verified' not in s] |
| skills = list(dict.fromkeys(skills)) |
| questions = [] |
| for skill in skills[:12]: |
| qdata = get_question_for_skill(skill) |
| questions.append({ |
| 'skill': skill, |
| 'question': qdata['question'], |
| 'rubric_keywords': qdata['rubric'][:5] |
| }) |
| return jsonify({'success': True, 'questions': questions, 'total': len(questions)}) |
|
|
| @app.route('/api/verify-skill', methods=['POST']) |
| @login_required |
| def verify_skill(): |
| data = request.json or {} |
| skill = data.get('skill') |
| answer = data.get('answer', '') |
| |
| if not skill or not answer: |
| return jsonify({'success': False, 'message': 'Missing skill or answer'}), 400 |
| |
| |
| verified, score = score_answer(skill, answer) |
| |
| user = get_current_user() |
| conn = get_db() |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| |
| all_verified = True |
| if resume: |
| skills = json.loads(resume['skills'] or '[]') |
| |
| skills = [s for s in skills if s.replace(' (Verified)', '') != skill] |
| if verified: |
| skills.append(f"{skill} (Verified)") |
| else: |
| skills.append(skill) |
| conn.execute("UPDATE resumes SET skills=? WHERE id=?", (json.dumps(skills), resume['id'])) |
| |
| raw = [s for s in skills if not s.endswith('(Verified)')] |
| ver = [s for s in skills if s.endswith('(Verified)')] |
| |
| claimed = list(dict.fromkeys([s.replace(' (Verified)', '') for s in skills])) |
| verified_skills = [s.replace(' (Verified)', '') for s in skills if s.endswith('(Verified)')] |
| ver_pct = (len(verified_skills) / max(len(claimed), 1)) * 100 |
| all_verified = ver_pct >= 80 and len(verified_skills) >= 1 |
| else: |
| all_verified = False |
| |
| |
| if all_verified: |
| conn.execute("UPDATE users SET verified=1 WHERE id=?", (user['id'],)) |
| conn.commit() |
| conn.close() |
| |
| return jsonify({ |
| 'success': True, |
| 'skill': skill, |
| 'verified': verified, |
| 'score': score, |
| 'profile_verified': all_verified, |
| 'message': f'{skill}: {"Verified" if verified else "Not verified"} ({score}%)' |
| }) |
|
|
| @app.route('/api/persist-verification', methods=['POST']) |
| @login_required |
| def persist_verification(): |
| """Persist interview results without re-scoring (used by multi-level interview).""" |
| data = request.json or {} |
| skill = data.get('skill') |
| verified = data.get('verified', False) |
| score = data.get('score', 0) |
| if not skill: |
| return jsonify({'success': False, 'message': 'Missing skill'}), 400 |
| |
| user = get_current_user() |
| conn = get_db() |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| |
| all_verified = False |
| if resume: |
| skills = json.loads(resume['skills'] or '[]') |
| skills = [s for s in skills if s.replace(' (Verified)', '') != skill] |
| if verified: |
| skills.append(f"{skill} (Verified)") |
| else: |
| skills.append(skill) |
| conn.execute("UPDATE resumes SET skills=? WHERE id=?", (json.dumps(skills), resume['id'])) |
| claimed = list(dict.fromkeys([s.replace(' (Verified)', '') for s in skills])) |
| verified_skills = [s.replace(' (Verified)', '') for s in skills if s.endswith('(Verified)')] |
| ver_pct = (len(verified_skills) / max(len(claimed), 1)) * 100 |
| all_verified = ver_pct >= 80 and len(verified_skills) >= 1 |
| else: |
| all_verified = False |
| |
| if all_verified: |
| conn.execute("UPDATE users SET verified=1 WHERE id=?", (user['id'],)) |
| conn.commit() |
| conn.close() |
| |
| return jsonify({ |
| 'success': True, |
| 'skill': skill, |
| 'verified': verified, |
| 'score': score, |
| 'profile_verified': all_verified, |
| 'message': f'{skill}: {"Verified" if verified else "Not verified"} ({score}%)' |
| }) |
|
|
| |
| def get_session_key(): |
| """Return a stable session key for interview state storage.""" |
| return str(session.get('user_id', 'anon')) |
|
|
| @app.route('/api/start-interview', methods=['POST']) |
| @login_required |
| def api_start_interview(): |
| data = request.json or {} |
| skill = data.get('skill') |
| if not skill: |
| return jsonify({'success': False, 'message': 'Missing skill'}), 400 |
| |
| session_key = get_session_key() |
| |
| try: |
| ai_question = generate_question_ai(skill, level=1) |
| except Exception: |
| ai_question = None |
| |
| if ai_question: |
| if session_key not in INTERVIEW_SESSIONS: |
| INTERVIEW_SESSIONS[session_key] = {} |
| |
| INTERVIEW_SESSIONS[session_key][skill] = { |
| 'current_level': 1, |
| 'total_levels': 3, |
| 'scores': [], |
| 'feedback': [], |
| 'questions': [ai_question], |
| 'answers': [], |
| 'passed': False, |
| 'ai_generated': True, |
| 'rubrics': [], |
| } |
| return jsonify({'success': True, 'interview': { |
| 'skill': skill, |
| 'level': 1, |
| 'total_levels': 3, |
| 'question': ai_question, |
| 'rubric_hints': _get_level_hints(skill, 1), |
| 'ai_generated': True, |
| 'level_name': 'Coding & Hands-On' |
| }}) |
| |
| result = start_interview(session_key, skill) |
| if not result: |
| return jsonify({'success': False, 'message': f'No interview available for {skill}'}), 404 |
| return jsonify({'success': True, 'interview': {**result, 'ai_generated': False}}) |
|
|
|
|
| def _get_level_hints(skill, level): |
| """Return helpful rubric hints for each level.""" |
| hints = { |
| 1: ["Write working code or pseudocode", "Handle edge cases", "Explain time/space complexity", "Show concrete input/output"], |
| 2: ["Identify the bug or issue", "Explain root cause step-by-step", "Provide the fix with code", "Discuss prevention strategies"], |
| 3: ["Architect a scalable solution", "Justify trade-offs", "Analyze performance characteristics", "Compare alternative approaches"], |
| } |
| return hints.get(level, []) |
|
|
|
|
| @app.route('/api/submit-answer', methods=['POST']) |
| @login_required |
| def api_submit_answer(): |
| data = request.json or {} |
| skill = data.get('skill') |
| answer = data.get('answer', '') |
| if not skill or not answer: |
| return jsonify({'success': False, 'message': 'Missing skill or answer'}), 400 |
| |
| session_key = get_session_key() |
| state = INTERVIEW_SESSIONS.get(session_key, {}).get(skill) |
| |
| try: |
| if state and state.get('ai_generated'): |
| current_level = state['current_level'] |
| current_q = state['questions'][current_level - 1] |
| |
| try: |
| ai_score, ai_verified, feedback = score_answer_ai(skill, current_q, answer, level=current_level) |
| except Exception: |
| ai_score, ai_verified, feedback = 50, False, f"AI scoring unavailable. Scored 50% based on response length." |
| |
| if ai_score is not None: |
| state['scores'].append(ai_score) |
| else: |
| state['scores'].append(50) |
| |
| state['feedback'].append(feedback or '') |
| state['answers'].append(answer) |
| state['current_level'] += 1 |
| |
| level_names = {1: 'Coding & Hands-On', 2: 'Debug & Troubleshooting', 3: 'Architecture & Deep Technical'} |
| |
| if state['current_level'] > state['total_levels']: |
| scores = state['scores'] |
| avg_score = sum(scores) / len(scores) |
| min_level_score = min(scores) |
| state['passed'] = avg_score >= 70 and min_level_score >= 55 |
| avg_score = min(avg_score, 100) |
| |
| breakdown = [] |
| for i in range(len(scores)): |
| breakdown.append({ |
| 'level': i + 1, |
| 'level_name': level_names.get(i + 1, f'Level {i+1}'), |
| 'score': scores[i], |
| 'feedback': state['feedback'][i], |
| 'passed': scores[i] >= 55, |
| 'question': state['questions'][i], |
| }) |
| |
| return jsonify({'success': True, 'result': { |
| 'done': True, |
| 'score': int(avg_score), |
| 'verified': state['passed'], |
| 'total_score': int(avg_score), |
| 'min_level_score': min_level_score, |
| 'message': f'{skill}: Verified! ({int(avg_score)}%)' if state['passed'] |
| else f'{skill}: Not verified ({int(avg_score)}% — need 70% avg with 55%+ each level)', |
| 'ai_generated': True, |
| 'breakdown': breakdown, |
| }}) |
| |
| try: |
| next_q = generate_question_ai(skill, level=state['current_level'], previous_answer=answer) |
| except Exception: |
| next_q = None |
| if not next_q: |
| next_q = { |
| 2: f"Walk me through a specific scenario where you used {skill} to solve a difficult problem. What was the problem, what was your approach, and what was the outcome?", |
| 3: f"Explain the internal workings of {skill} at a deep level. How does it handle memory, concurrency, or edge cases? What are its limitations?" |
| }.get(state['current_level'], f"Describe a challenging problem you solved using {skill}.") |
| state['questions'].append(next_q) |
| |
| level_passed = ai_score >= 55 if ai_score else False |
| emoji = "✓" if level_passed else "✗" |
| |
| return jsonify({'success': True, 'result': { |
| 'done': False, |
| 'level': state['current_level'], |
| 'total_levels': state['total_levels'], |
| 'question': next_q, |
| 'rubric_hints': _get_level_hints(skill, state['current_level']), |
| 'ai_generated': True, |
| 'level_name': level_names.get(state['current_level'], f'Level {state["current_level"]}'), |
| 'previous_level_result': { |
| 'score': ai_score, |
| 'passed': level_passed, |
| 'feedback': feedback or '', |
| 'level_completed': current_level, |
| 'emoji': emoji, |
| } |
| }}) |
| |
| rb = continue_interview(session_key, skill, answer) |
| if 'error' in rb: |
| return jsonify({'success': False, 'message': rb['error']}), 400 |
| |
| is_done = not rb.get('interview_ongoing', True) |
| score_for_level = rb.get('score_for_level', 50) |
| level_completed = rb.get('level_completed') or rb.get('levels_completed', 1) |
| |
| result = { |
| 'done': is_done, |
| 'level': level_completed, |
| 'total_levels': rb.get('total_levels', 3), |
| 'ai_generated': False, |
| } |
| |
| if not is_done: |
| result['previous_level_result'] = { |
| 'score': score_for_level, |
| 'passed': rb.get('passed_level', False), |
| 'feedback': f"Score: {score_for_level}%", |
| 'level_completed': level_completed, |
| } |
| result['level_name'] = f"Level {level_completed}" |
| result['question'] = rb.get('next_question', '') |
| result['rubric_hints'] = rb.get('next_rubric_keywords', []) |
| else: |
| scores = rb.get('scores', []) |
| avg = rb.get('average_score', 0) |
| min_score = min(scores) if scores else 0 |
| passed = rb.get('passed', False) |
| level_names = {1: 'Coding & Hands-On', 2: 'Debug & Troubleshooting', 3: 'Architecture & Deep Technical'} |
| breakdown = [] |
| for i, sc in enumerate(scores): |
| breakdown.append({ |
| 'level': i + 1, |
| 'level_name': level_names.get(i + 1, f'Level {i+1}'), |
| 'score': sc, |
| 'feedback': '', |
| 'passed': sc >= 55, |
| 'question': '', |
| }) |
| result['score'] = int(avg) |
| result['total_score'] = int(avg) |
| result['verified'] = passed |
| result['min_level_score'] = min_score |
| result['message'] = f'{skill}: Verified! ({int(avg)}%)' if passed else f'{skill}: Not verified ({int(avg)}%)' |
| result['breakdown'] = breakdown |
| |
| return jsonify({'success': True, 'result': result}) |
| |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| return jsonify({'success': False, 'message': f'Server error: {str(e)[:200]}'}), 500 |
|
|
| @app.route('/api/reset-interview', methods=['POST']) |
| @login_required |
| def api_reset_interview(): |
| data = request.json or {} |
| skill = data.get('skill') |
| if skill: |
| session_key = get_session_key() |
| reset_interview(session_key, skill) |
| return jsonify({'success': True}) |
|
|
| |
| @app.route('/api/jobs', methods=['GET', 'POST']) |
| def jobs(): |
| if request.method == 'GET': |
| conn = get_db() |
| rows = conn.execute(""" |
| SELECT j.*, u.username as hirer_name |
| FROM jobs j JOIN users u ON j.hirer_id=u.id |
| WHERE j.active=1 ORDER BY j.created_at DESC |
| """).fetchall() |
| conn.close() |
| result = [] |
| for r in rows: |
| d = dict(r) |
| d['required_skills'] = json.loads(d.get('required_skills', '[]')) |
| result.append(d) |
| return jsonify({'success': True, 'jobs': result}) |
| else: |
| data = request.json or {} |
| user = get_current_user() |
| if not user or user['role'] != 'hirer': |
| return jsonify({'success': False, 'message': 'Only hirers can post jobs'}), 403 |
| conn = get_db() |
| conn.execute(""" |
| INSERT INTO jobs (hirer_id, company, title, description, location, employment_type, salary_min, salary_max, required_skills) |
| VALUES (?,?,?,?,?,?,?,?,?) |
| """, (user['id'], user.get('company',''), data.get('title'), data.get('description'), |
| data.get('location'), data.get('employment_type','Full-time'), |
| _safe_int(data.get('salary_min',0)), _safe_int(data.get('salary_max',0)), |
| json.dumps(data.get('required_skills',[])))) |
| conn.commit() |
| job_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] |
| conn.close() |
| return jsonify({'success': True, 'job_id': job_id}) |
|
|
| |
| @app.route('/api/trending-skills') |
| def trending_skills(): |
| conn = get_db() |
| rows = conn.execute("SELECT * FROM trending_skills ORDER BY frequency DESC LIMIT 8").fetchall() |
| conn.close() |
| if not rows: |
| scan_trending_skills() |
| conn = get_db() |
| rows = conn.execute("SELECT * FROM trending_skills ORDER BY frequency DESC LIMIT 8").fetchall() |
| conn.close() |
| return jsonify({'success': True, 'trending_skills': [dict(r) for r in rows]}) |
|
|
| @app.route('/api/scan-trending', methods=['POST']) |
| @login_required |
| def api_scan_trending(): |
| scan_trending_skills() |
| return jsonify({'success': True, 'message': 'Trending skills updated'}) |
|
|
| |
| @app.route('/api/apply', methods=['POST']) |
| @login_required |
| def apply_for_job(): |
| data = request.json or {} |
| job_id = data.get('job_id') |
| user = get_current_user() |
| conn = get_db() |
| job = conn.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone() |
| if not job: |
| conn.close() |
| return jsonify({'success': False, 'message': 'Job not found'}), 404 |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (user['id'],)).fetchone() |
| skills = json.loads(resume['skills']) if resume and resume['skills'] else [] |
| resume_filename = resume['filename'] if resume else '' |
| |
| required = json.loads(job['required_skills'] or '[]') |
| if required: |
| required_lower = {s.strip().lower() for s in required} |
| skills_lower = {s.replace(' (Verified)', '').strip().lower() for s in skills} |
| matched = len(required_lower & skills_lower) |
| skill_pct = (matched / len(required_lower)) * 100 if required_lower else 50 |
| match_score = min(95, round(skill_pct * 0.7 + (20 if user['verified'] else 10) + min(len(skills) * 1.5, 15))) |
| else: |
| match_score = min(95, 60 + len(skills) * 3 + (10 if user['verified'] else 0)) |
| cur = conn.execute(""" |
| INSERT INTO applications (job_id, seeker_id, seeker_name, seeker_email, resume_filename, skills, match_score, status) |
| VALUES (?,?,?,?,?,?,?,?) |
| """, (job_id, user['id'], user['username'], user['email'], resume_filename, json.dumps(skills), match_score, 'applied')) |
| conn.commit() |
| application_id = cur.lastrowid |
| conn.close() |
| return jsonify({'success': True, 'application_id': application_id, 'match_score': match_score}) |
|
|
| |
| @app.route('/api/my-jobs') |
| @login_required |
| def get_my_jobs(): |
| user = get_current_user() |
| if user['role'] != 'hirer': |
| return jsonify({'success': False, 'message': 'Hirers only'}), 403 |
| conn = get_db() |
| rows = conn.execute(""" |
| SELECT j.*, u.username as hirer_name |
| FROM jobs j JOIN users u ON j.hirer_id=u.id |
| WHERE j.hirer_id=? AND j.active=1 ORDER BY j.created_at DESC |
| """, (user['id'],)).fetchall() |
| conn.close() |
| result = [] |
| for r in rows: |
| d = dict(r) |
| d['required_skills'] = json.loads(d.get('required_skills', '[]')) |
| result.append(d) |
| return jsonify({'success': True, 'jobs': result}) |
|
|
| |
| @app.route('/api/applicants') |
| @login_required |
| def get_applicants(): |
| user = get_current_user() |
| if user['role'] != 'hirer': |
| return jsonify({'success': False, 'message': 'Hirers only'}), 403 |
| conn = get_db() |
| rows = conn.execute(""" |
| SELECT a.*, j.title as job_title, j.company, |
| u.username as seeker_name, u.email as seeker_email, |
| p.phone, p.location, p.headline, p.years_of_experience, |
| p.experience as prof_experience, p.education as prof_education, |
| p.certifications as prof_certifications, |
| p.linkedin_url, p.github_url, p.portfolio_url, |
| r.skills, r.filename as resume_filename |
| FROM applications a |
| JOIN jobs j ON a.job_id=j.id |
| JOIN users u ON a.seeker_id=u.id |
| LEFT JOIN profiles p ON a.seeker_id=p.user_id |
| LEFT JOIN (SELECT user_id, skills, filename FROM resumes ORDER BY uploaded_at DESC) r ON a.seeker_id=r.user_id |
| WHERE j.hirer_id=? |
| GROUP BY a.id |
| ORDER BY a.match_score DESC |
| """, (user['id'],)).fetchall() |
| conn.close() |
| return jsonify({'success': True, 'applicants': [dict(r) for r in rows]}) |
|
|
| |
| @app.route('/api/applicants/<int:app_id>/status', methods=['PATCH']) |
| @login_required |
| def update_applicant_status(app_id): |
| user = get_current_user() |
| if user['role'] != 'hirer': |
| return jsonify({'success': False, 'message': 'Hirers only'}), 403 |
| data = request.json or {} |
| status = data.get('status') |
| if status not in ('applied', 'interview', 'offer', 'rejected'): |
| return jsonify({'success': False, 'message': 'Invalid status'}), 400 |
| conn = get_db() |
| conn.execute("UPDATE applications SET status=? WHERE id=?", (status, app_id)) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True, 'status': status}) |
|
|
| |
| @app.route('/profile') |
| def profile_page(): |
| if not session.get('user_id'): |
| return redirect(url_for('login_page')) |
| return render_template('profile.html') |
|
|
| @app.route('/api/update-profile', methods=['POST']) |
| @login_required |
| def update_profile(): |
| user = get_current_user() |
| data = request.json or {} |
| conn = get_db() |
| updates = [] |
| params = [] |
| for field in ('username', 'company', 'company_desc', 'company_size', 'company_website', 'company_logo'): |
| if field in data: |
| updates.append(f"{field}=?") |
| params.append(data[field]) |
| if updates: |
| params.append(user['id']) |
| conn.execute(f"UPDATE users SET {', '.join(updates)} WHERE id=?", params) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True}) |
|
|
| |
| @app.route('/api/admin/stats') |
| @login_required |
| def admin_stats(): |
| user = get_current_user() |
| if user['role'] != 'admin' and user['id'] != 1: |
| return jsonify({'success': False, 'message': 'Admins only'}), 403 |
| conn = get_db() |
| users_count = conn.execute("SELECT COUNT(*) as c FROM users").fetchone()['c'] |
| seekers_count = conn.execute("SELECT COUNT(*) as c FROM users WHERE role='seeker'").fetchone()['c'] |
| hirers_count = conn.execute("SELECT COUNT(*) as c FROM users WHERE role='hirer'").fetchone()['c'] |
| jobs_count = conn.execute("SELECT COUNT(*) as c FROM jobs WHERE active=1").fetchone()['c'] |
| apps_count = conn.execute("SELECT COUNT(*) as c FROM applications").fetchone()['c'] |
| verified_count = conn.execute("SELECT COUNT(*) as c FROM users WHERE verified=1").fetchone()['c'] |
| reported = conn.execute("SELECT COUNT(*) as c FROM jobs WHERE reported>0").fetchone()['c'] |
| conn.close() |
| return jsonify({'success': True, 'stats': { |
| 'users': users_count, 'seekers': seekers_count, 'hirers': hirers_count, |
| 'jobs': jobs_count, 'applications': apps_count, |
| 'verified': verified_count, 'verified_pct': round(verified_count/max(users_count,1)*100), |
| 'reported_jobs': reported |
| }}) |
|
|
| @app.route('/api/admin/users') |
| @login_required |
| def admin_users(): |
| user = get_current_user() |
| if user['role'] != 'admin' and user['id'] != 1: |
| return jsonify({'success': False, 'message': 'Admins only'}), 403 |
| conn = get_db() |
| rows = conn.execute("SELECT id, username, email, role, company, verified, created_at FROM users ORDER BY created_at DESC").fetchall() |
| conn.close() |
| return jsonify({'success': True, 'users': [dict(r) for r in rows]}) |
|
|
| @app.route('/api/admin/jobs') |
| @login_required |
| def admin_jobs(): |
| user = get_current_user() |
| if user['role'] != 'admin' and user['id'] != 1: |
| return jsonify({'success': False, 'message': 'Admins only'}), 403 |
| conn = get_db() |
| rows = conn.execute(""" |
| SELECT j.*, u.username as hirer_name FROM jobs j |
| JOIN users u ON j.hirer_id=u.id ORDER BY j.created_at DESC |
| """).fetchall() |
| conn.close() |
| result = [] |
| for r in rows: |
| d = dict(r) |
| d['required_skills'] = json.loads(d.get('required_skills', '[]')) |
| result.append(d) |
| return jsonify({'success': True, 'jobs': result}) |
|
|
| @app.route('/api/admin/jobs/<int:job_id>/toggle', methods=['POST']) |
| @login_required |
| def toggle_job(job_id): |
| user = get_current_user() |
| if user['role'] != 'admin' and user['id'] != 1: |
| return jsonify({'success': False, 'message': 'Admins only'}), 403 |
| conn = get_db() |
| job = conn.execute("SELECT active FROM jobs WHERE id=?", (job_id,)).fetchone() |
| if job: |
| conn.execute("UPDATE jobs SET active=? WHERE id=?", (0 if job['active'] else 1, job_id)) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True}) |
|
|
| @app.route('/api/admin/users/<int:user_id>/delete', methods=['POST']) |
| @login_required |
| def admin_delete_user(user_id): |
| user = get_current_user() |
| if user['role'] != 'admin' and user['id'] != 1: |
| return jsonify({'success': False, 'message': 'Admins only'}), 403 |
| if user_id == 1: |
| return jsonify({'success': False, 'message': 'Cannot delete root admin'}), 400 |
| conn = get_db() |
| conn.execute("DELETE FROM applications WHERE seeker_id=?", (user_id,)) |
| conn.execute("DELETE FROM resumes WHERE user_id=?", (user_id,)) |
| conn.execute("UPDATE jobs SET active=0 WHERE hirer_id=?", (user_id,)) |
| conn.execute("DELETE FROM users WHERE id=?", (user_id,)) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True}) |
|
|
| |
| @app.route('/api/hirer/analytics') |
| @login_required |
| def hirer_analytics(): |
| user = get_current_user() |
| if user['role'] != 'hirer': |
| return jsonify({'success': False, 'message': 'Hirers only'}), 403 |
| conn = get_db() |
| jobs = conn.execute("SELECT id, title, job_views FROM jobs WHERE hirer_id=? AND active=1", (user['id'],)).fetchall() |
| total_views = sum(j['job_views'] or 0 for j in jobs) |
| apps = conn.execute(""" |
| SELECT a.status, COUNT(*) as c FROM applications a |
| JOIN jobs j ON a.job_id=j.id WHERE j.hirer_id=? |
| GROUP BY a.status |
| """, (user['id'],)).fetchall() |
| status_breakdown = {r['status']: r['c'] for r in apps} |
| conn.close() |
| return jsonify({'success': True, 'analytics': { |
| 'total_jobs': len(jobs), |
| 'total_views': total_views, |
| 'status_breakdown': status_breakdown, |
| 'jobs': [dict(j) for j in jobs] |
| }}) |
|
|
| |
| @app.route('/api/jobs/<int:job_id>/view', methods=['POST']) |
| def track_job_view(job_id): |
| conn = get_db() |
| conn.execute("UPDATE jobs SET job_views = COALESCE(job_views, 0) + 1 WHERE id=?", (job_id,)) |
| conn.commit() |
| conn.close() |
| return jsonify({'success': True}) |
|
|
| |
| @app.route('/api/resume/<int:seeker_id>') |
| @login_required |
| def get_resume(seeker_id): |
| user = get_current_user() |
| signed_uid = request.args.get('uid') |
| signed_exp = request.args.get('exp') |
| signed_sig = request.args.get('sig') |
| |
| |
| has_signed = signed_uid and signed_exp and signed_sig |
| valid_signed = has_signed and verify_signed_url(seeker_id, signed_uid, signed_exp, signed_sig) |
| |
| if not valid_signed and user['role'] != 'hirer' and user['id'] != seeker_id: |
| return jsonify({'success': False, 'message': 'Unauthorized'}), 403 |
| |
| conn = get_db() |
| resume = conn.execute("SELECT * FROM resumes WHERE user_id=? ORDER BY uploaded_at DESC LIMIT 1", (seeker_id,)).fetchone() |
| conn.close() |
| if not resume or not resume['filename']: |
| return jsonify({'success': False, 'message': 'No resume found'}), 404 |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{seeker_id}_{resume['filename']}") |
| if not os.path.exists(filepath): |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], resume['filename']) |
| if not os.path.exists(filepath): |
| return jsonify({'success': False, 'message': 'File not found on disk'}), 404 |
| return send_from_directory(app.config['UPLOAD_FOLDER'], os.path.basename(filepath), |
| download_name=resume['filename'], as_attachment=False) |
|
|
| @app.route('/api/resume/<int:seeker_id>/signed-url') |
| @login_required |
| def resume_signed_url(seeker_id): |
| """Generate a signed URL for resume access (privacy-preserving).""" |
| user = get_current_user() |
| if user['role'] != 'hirer' and user['id'] != seeker_id: |
| return jsonify({'success': False, 'message': 'Unauthorized'}), 403 |
| signed = generate_signed_url(seeker_id, user['id']) |
| return jsonify({'success': True, 'signed_url': signed}) |
|
|
| |
| TRIVIAL_REPLIES = {'hey', 'hello', 'hi', 'hey there', 'hello there', 'hi there', 'hey!', |
| 'hello!', 'hi!', 'howdy', 'hey there!', 'hello there!', 'hi there!', |
| 'i am nexus', 'i am your assistant', 'i am an ai', "i'm an ai", "i'm nexus", |
| 'how can i help', 'what can i do', 'how may i help', 'what do you need'} |
|
|
| CHATBOT_AI_PROMPT = ( |
| "You are Nexus Assistant, a helpful AI for Nexus ATS — a job marketplace and skill verification platform. " |
| "You can answer ANY question — platform help, general knowledge, math, science, coding, career advice, etc. " |
| "Be accurate, concise, and helpful. For math/science, show your reasoning. " |
| "For coding questions, provide working code examples. For platform questions, give step-by-step instructions. " |
| "Keep responses under 200 words unless more detail is needed." |
| ) |
|
|
| def _is_trivial_reply(reply, user_message): |
| """Check if a generated reply is just a greeting or too trivial to be useful.""" |
| clean = reply.lower().strip().rstrip('.!?').strip() |
| if clean in TRIVIAL_REPLIES: |
| return True |
| words = clean.split() |
| if len(words) <= 3: |
| if any(c.isdigit() for c in reply): |
| return False |
| if user_message and any(kw in user_message.lower() for kw in ['what is', 'how many', 'how much', 'what time', 'who', 'when', 'where']): |
| return False |
| return True |
| return False |
|
|
| def generate_llm_chat(message, chat_history=None): |
| """Use HF Inference API for general Q&A chatbot. |
| Returns (reply_text, status) where status is 'ok' or 'failed'.""" |
| try: |
| from ai_pipeline import call_llm |
| messages = [{"role": "system", "content": CHATBOT_AI_PROMPT}] |
| if chat_history: |
| for ex in chat_history[-3:]: |
| messages.append({"role": "user", "content": ex['q']}) |
| messages.append({"role": "assistant", "content": ex['a']}) |
| messages.append({"role": "user", "content": message}) |
|
|
| reply = call_llm(messages, max_tokens=256, temperature=0.7) |
| if reply and len(reply) > 3 and not _is_trivial_reply(reply, message): |
| return reply, 'ok' |
| return None, 'failed' |
| except Exception as e: |
| print(f" [CHAT] AI API error: {e}") |
| return None, 'failed' |
|
|
| |
| import math as _math |
|
|
| def _safe_math_eval(expr): |
| """Safely evaluate a math expression. Returns (result, None) or (None, error).""" |
| allowed = set('0123456789+-*/.() %') |
| clean = expr.strip().rstrip('?').strip() |
| clean = clean.replace('^', '**').replace('×', '*').replace('÷', '/') |
| clean = clean.replace('sqrt', '_SQRT_').replace('sqrt of', '_SQRT_') |
| if not clean: |
| return None, 'empty' |
| if not all(c in allowed or c == '_' for c in clean): |
| return None, 'invalid' |
| try: |
| result = eval(clean, {"__builtins__": {}, "_SQRT_": _math.sqrt}) |
| return result, None |
| except Exception: |
| return None, 'error' |
|
|
| def _try_math_answer(message): |
| """Try to detect and answer math questions. Returns answer string or None.""" |
| msg = message.lower().strip().rstrip('?').strip() |
| import re as _re |
| math_patterns = [ |
| (r'^what\s+is\s+(.+)$', lambda m: m.group(1)), |
| (r'^calculate\s+(.+)$', lambda m: m.group(1)), |
| (r'^compute\s+(.+)$', lambda m: m.group(1)), |
| (r'^solve\s+(.+)$', lambda m: m.group(1)), |
| (r'^how\s+much\s+is\s+(.+)$', lambda m: m.group(1)), |
| (r'^(.+)\s+equals?\s*$', lambda m: m.group(1)), |
| ] |
| for pattern, extractor in math_patterns: |
| match = _re.match(pattern, msg) |
| if match: |
| expr = extractor(match).strip() |
| result, err = _safe_math_eval(expr) |
| if result is not None: |
| if isinstance(result, float) and result == int(result): |
| result = int(result) |
| return f"**{result}**" |
| |
| bare = msg.replace('x', '*').replace('×', '*').replace('÷', '/').replace('^', '**') |
| if bare and all(c in '0123456789+-*/.() **' for c in bare): |
| result, err = _safe_math_eval(bare) |
| if result is not None and err is None: |
| if isinstance(result, float) and result == int(result): |
| result = int(result) |
| return f"**{result}**" |
| return None |
|
|
| |
| GENERAL_KNOWLEDGE = [ |
| { |
| 'keywords': ['what is python', 'python programming', 'explain python', 'about python'], |
| 'reply': "**Python** is a high-level, interpreted programming language known for its simple syntax and readability. It's widely used for:\n\n• **Web Development** — Django, Flask, FastAPI\n• **Data Science** — Pandas, NumPy, Matplotlib\n• **AI/ML** — TensorFlow, PyTorch, scikit-learn\n• **Automation** — scripting, web scraping, DevOps\n• **Desktop Apps** — PyQt, Tkinter\n\nPython is beginner-friendly and one of the most popular languages worldwide, ranking #1 on the TIOBE index." |
| }, |
| { |
| 'keywords': ['what is javascript', 'javascript programming', 'explain javascript', 'about javascript'], |
| 'reply': "**JavaScript** is the language of the web. It runs in every browser and on servers via Node.js. Key features:\n\n• **Frontend** — React, Vue, Angular for interactive UIs\n• **Backend** — Node.js, Express, Fastify\n• **Mobile** — React Native, Ionic\n• **Desktop** — Electron (VS Code is built with it)\n• **Full-Stack** — MERN, MEAN stacks\n\nES6+ brought modern features like arrow functions, async/await, destructuring, and modules." |
| }, |
| { |
| 'keywords': ['what is react', 'explain react', 'about react', 'react framework'], |
| 'reply': "**React** is a JavaScript library for building user interfaces, created by Meta (Facebook). Key concepts:\n\n• **Components** — Reusable UI pieces (functional + hooks)\n• **Virtual DOM** — Efficient diffing and updates\n• **JSX** — HTML-like syntax in JavaScript\n• **Hooks** — useState, useEffect, useContext, useRef\n• **Ecosystem** — Next.js (SSR/SSG), React Router, Redux\n\nReact uses a one-way data flow and component-based architecture. It's the most popular frontend library worldwide." |
| }, |
| { |
| 'keywords': ['what is docker', 'explain docker', 'about docker', 'containerization'], |
| 'reply': "**Docker** is a platform for building, shipping, and running applications in containers. Containers package code with all dependencies so it runs the same everywhere.\n\n• **Dockerfile** — Blueprint for building images\n• **Image** — Read-only template with your app + deps\n• **Container** — Running instance of an image\n• **Docker Compose** — Define multi-container apps\n• **Benefits** — Consistent dev/prod, isolation, fast deploys\n\nContainers are lighter than VMs and start in seconds." |
| }, |
| { |
| 'keywords': ['what is api', 'explain api', 'about api', 'rest api', 'what are apis'], |
| 'reply': "**API** (Application Programming Interface) is a set of rules that lets software applications communicate with each other.\n\n**REST API** (most common):\n• **GET** — Read data\n• **POST** — Create data\n• **PUT/PATCH** — Update data\n• **DELETE** — Remove data\n\nExample: `GET /api/users/123` returns user data as JSON.\n\nOther types: GraphQL, WebSocket, gRPC, SOAP. APIs are the backbone of modern web and mobile apps." |
| }, |
| { |
| 'keywords': ['what is sql', 'explain sql', 'about sql', 'database query'], |
| 'reply': "**SQL** (Structured Query Language) is used to manage and query relational databases. Core commands:\n\n• **SELECT** — Retrieve data\n• **INSERT** — Add data\n• **UPDATE** — Modify data\n• **DELETE** — Remove data\n• **JOIN** — Combine tables\n• **GROUP BY** — Aggregate results\n\nExample: `SELECT name, SUM(amount) FROM orders GROUP BY name HAVING SUM(amount) > 1000;`\n\nPopular databases: PostgreSQL, MySQL, SQLite, SQL Server." |
| }, |
| { |
| 'keywords': ['what is machine learning', 'explain machine learning', 'about ml', 'what is ai', 'artificial intelligence'], |
| 'reply': "**Machine Learning** is a subset of AI where systems learn patterns from data to make predictions without being explicitly programmed.\n\n**Types:**\n• **Supervised** — Labeled data (classification, regression)\n• **Unsupervised** — No labels (clustering, dimensionality reduction)\n• **Reinforcement** — Learning from rewards/penalties\n\n**Common algorithms:** Linear Regression, Decision Trees, Random Forest, Neural Networks, SVMs\n\n**Tools:** Python, scikit-learn, TensorFlow, PyTorch, Pandas" |
| }, |
| { |
| 'keywords': ['what is git', 'explain git', 'about git', 'version control'], |
| 'reply': "**Git** is a distributed version control system for tracking code changes. Key commands:\n\n• `git init` — Start a repo\n• `git add .` — Stage changes\n• `git commit -m 'msg'` — Save changes\n• `git push` — Upload to remote\n• `git pull` — Download from remote\n• `git branch` — Create/switch branches\n• `git merge` — Combine branches\n\nPlatforms: GitHub, GitLab, Bitbucket. Git tracks every change ever made, so you can always go back." |
| }, |
| { |
| 'keywords': ['what is kubernetes', 'explain kubernetes', 'about kubernetes', 'what is k8s'], |
| 'reply': "**Kubernetes (K8s)** is an open-source container orchestration platform by Google. It automates deployment, scaling, and management of containerized apps.\n\n**Core concepts:**\n• **Pod** — Smallest deployable unit (1+ containers)\n• **Deployment** — Manages replica sets\n• **Service** — Network endpoint for pods\n• **Ingress** — HTTP routing\n• **ConfigMap/Secret** — Configuration management\n\nKubernetes handles auto-scaling, self-healing, rolling updates, and load balancing automatically." |
| }, |
| { |
| 'keywords': ['what is cloud computing', 'explain cloud', 'about cloud', 'aws azure gcp'], |
| 'reply': "**Cloud Computing** delivers computing resources (servers, storage, databases, networking) over the internet on-demand.\n\n**Major providers:**\n• **AWS** — Most popular (EC2, S3, Lambda, RDS)\n• **Azure** — Microsoft's cloud (good for .NET/enterprise)\n• **GCP** — Google Cloud (BigQuery, Kubernetes origin)\n\n**Service models:**\n• **IaaS** — Virtual machines (EC2)\n• **PaaS** — Managed platforms (Heroku, App Engine)\n• **SaaS** — Software as a service (Gmail, Slack)\n\n**Benefits:** Pay-as-you-go, scale on demand, no hardware management." |
| }, |
| { |
| 'keywords': ['who created', 'who invented', 'who made', 'who developed', 'who is the founder', 'who is the creator'], |
| 'reply': "I can answer specific 'who created X' questions! Try asking:\n\n• \"Who created Python?\"\n• \"Who invented the World Wide Web?\"\n• \"Who created Linux?\"\n• \"Who founded GitHub?\"\n\nJust replace X with the technology or product you're curious about!" |
| }, |
| { |
| 'keywords': ['difference between', 'vs', 'versus', 'compared to', 'compare'], |
| 'reply': "I can compare technologies! Try asking about specific comparisons:\n\n• \"SQL vs NoSQL\"\n• \"React vs Angular vs Vue\"\n• \"Python vs JavaScript\"\n• \"REST vs GraphQL\"\n• \"Docker vs Kubernetes\"\n• \"AWS vs Azure vs GCP\"\n\nBe specific about what you'd like compared!" |
| }, |
| { |
| 'keywords': ['how to learn', 'where to learn', 'best way to learn', 'learning path', 'roadmap', 'study guide'], |
| 'reply': "Here are some great free resources for tech learning:\n\n**Coding:**\n• freeCodeCamp.org — Full curriculum (HTML/CSS/JS/Python)\n• The Odin Project — Full-stack web dev\n• CS50 (Harvard) — Computer science fundamentals\n\n**Practice:**\n• LeetCode / HackerRank — Coding challenges\n• Exercism — Language exercises with mentoring\n\n**Documentation:**\n• MDN Web Docs — Web technology reference\n• Python.org — Official Python docs\n\n**YouTube:** Fireship, Traversy Media, The Net Ninja\n\nPick ONE path and build projects — that's the fastest way to learn!" |
| }, |
| { |
| 'keywords': ['what salary', 'how much salary', 'average salary', 'salary for', 'pay for', 'earn as'], |
| 'reply': "Tech salaries vary by location, experience, and company. Here are **US average** ranges:\n\n• **Junior Developer** — $60K-$80K\n• **Mid-Level Developer** — $90K-$130K\n• **Senior Developer** — $130K-$180K\n• **Staff/Principal Engineer** — $180K-$300K+\n• **ML Engineer** — $120K-$200K\n• **DevOps Engineer** — $110K-$170K\n\n**Top-paying skills:** AWS, Kubernetes, ML/AI, Go, Rust\n\nCheck the **Trending Skills** section on your Seeker Dashboard for real-time salary data from job postings on this platform!" |
| }, |
| ] |
|
|
| |
| CHATBOT_TOPICS = [ |
| |
| { |
| 'keywords': ['hello', 'hi', 'hey', 'hiya', 'howdy', 'greetings', 'good morning', 'good evening', 'good afternoon', 'sup', 'yo', 'hola'], |
| 'reply': "Hey there! Welcome to Nexus ATS! I'm your AI assistant — here to help you find jobs, verify skills, post listings, or anything else on the platform. What can I help you with today?" |
| }, |
| { |
| 'keywords': ['whats up', "what's up", 'wassup', 'howdy', 'how are you doing'], |
| 'reply': "Hey! Not much — just here and ready to help! Whether you're looking to find a job, verify your skills, or post a listing, I've got you covered. What's on your mind?" |
| }, |
| |
| { |
| 'keywords': ['how are you', 'how r u', 'how are u', 'you good', 'you doing ok', 'how do you do'], |
| 'reply': "I'm doing great, thanks for asking! 😊 I'm running smooth and ready to help. How about you — anything I can assist with on Nexus ATS?" |
| }, |
| { |
| 'keywords': ['what can you do', 'what do you do', 'your purpose', 'why are you here', 'who are you', 'what are you', 'tell me about yourself', 'introduce yourself'], |
| 'reply': "I'm **Nexus Assistant**, your AI helper for the Nexus ATS platform! I can help you:\n\n🔍 Find and apply for jobs\n✅ Verify your skills with AI interviews\n📝 Post job listings\n👥 Manage applicants\n📊 Check trending skills\n💼 Update your profile\n\nJust ask me anything about the platform!" |
| }, |
| { |
| 'keywords': ['are you real', 'are you a bot', 'are you human', 'are you ai', 'are you a robot'], |
| 'reply': "I'm an AI assistant built for Nexus ATS! I'm not human, but I do my best to be helpful and friendly. I know a lot about how this platform works, so fire away with your questions!" |
| }, |
| { |
| 'keywords': ['what is your name', 'your name', 'whats your name', "what's your name", 'call you'], |
| "reply": "I'm **Nexus Assistant** — your friendly AI helper for the Nexus ATS platform! You can just call me Nexus. 😊" |
| }, |
| |
| { |
| 'keywords': ['thank', 'thanks', 'thx', 'ty', 'tysm', 'appreciate', 'cheers', 'kudos'], |
| 'reply': "You're welcome! Happy to help. Is there anything else you'd like to know about Nexus ATS?" |
| }, |
| { |
| 'keywords': ['thanks a lot', 'thank you so much', 'you are great', 'you are awesome', 'you are the best', 'great job', 'well done'], |
| 'reply': "Aww, thank you! That means a lot! 😊 I'm always here if you need anything else on the platform. Good luck with your job search or hiring!" |
| }, |
| |
| { |
| 'keywords': ['bye', 'goodbye', 'see you', 'later', 'gotta go', 'take care', 'peace out', 'exit'], |
| 'reply': "Goodbye! It was great chatting with you. Good luck with your job search or hiring — I'll be here if you need me! 👋" |
| }, |
| |
| { |
| 'keywords': ['joke', 'tell me a joke', 'make me laugh', 'something funny', 'humor'], |
| 'reply': "Sure! Here's one: Why do programmers prefer dark mode? Because light attracts bugs! 🐛😄\n\nWant another one, or can I help with something on the platform?" |
| }, |
| { |
| 'keywords': ['another joke', 'one more joke', 'funny'], |
| 'reply': "Here's another: A SQL query walks into a bar, sees two tables, and asks... \"Can I JOIN you?\" 🍻😄\n\nNeed more laughs or ready to get back to business?" |
| }, |
| |
| { |
| 'keywords': ['i am stressed', 'i feel lost', 'i need help', 'i dont know', "don't know what to do", 'overwhelmed', 'anxious'], |
| 'reply': "I hear you — job searching or hiring can be overwhelming. Take it one step at a time! Start by uploading your resume on the Seeker Dashboard, and I'll guide you through skill verification. You've got this! 💪" |
| }, |
| { |
| 'keywords': ['wish me luck', 'good luck to me', 'fingers crossed'], |
| 'reply': 'Good luck! 🍀 You\u0027ve got this. Whether it\u0027s landing that dream job or finding the perfect candidate, Nexus ATS is here to help you succeed!' |
| }, |
| |
| { |
| 'keywords': ['find job', 'search job', 'browse job', 'open position', 'look for job', 'job listing', 'available job', 'apply job', 'how to apply', 'looking for work', 'need a job', 'job search'], |
| 'reply': ( |
| "To find and apply for jobs:\n\n" |
| "1️⃣ Go to your **Seeker Dashboard** (click your name in the nav bar, then Dashboard)\n" |
| "2️⃣ Scroll down to the **Open positions** section\n" |
| "3️⃣ Browse active job listings with match scores\n" |
| "4️⃣ Click **Apply** on any job that interests you\n\n" |
| "Your match score is calculated based on your verified skills, so make sure to upload your resume and verify your skills first for the best results!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['post job', 'create job', 'new job', 'add job', 'list job', 'hire', 'job posting', 'i want to hire', 'hire someone'], |
| 'reply': ( |
| "To post a new job:\n\n" |
| "1️⃣ Log in with a **hirer** account\n" |
| "2️⃣ Go to your **Hirer Dashboard**\n" |
| "3️⃣ Fill in the **Post a new job** form with:\n" |
| " • Job title\n" |
| " • Company name\n" |
| " • Location (or Remote)\n" |
| " • Salary range\n" |
| " • Description\n" |
| " • Required skills\n" |
| "4️⃣ Click **Post Job** — it'll appear immediately in the marketplace!\n\n" |
| "You can manage all your listings from the **My Jobs** section on the dashboard." |
| ) |
| }, |
| |
| { |
| 'keywords': ['skill verification', 'verify skill', 'verify my skill', 'skill badge', 'skill test', 'skill interview', 'ai question', 'verification process', 'how verification work', 'earn badge', 'get verified', 'verified', 'verify'], |
| 'reply': ( |
| "Our **3-Level AI Skill Verification** works like this:\n\n" |
| "📄 **Step 1**: Upload your resume (PDF or DOCX) on the seeker dashboard\n" |
| "🔍 **Step 2**: Our system extracts your skills automatically\n" |
| "🎯 **Step 3**: For each skill, you answer **3 technical questions**:\n" |
| " • **Level 1** — Coding problem (write real code)\n" |
| " • **Level 2** — Debugging challenge (find and fix the bug)\n" |
| " • **Level 3** — Architecture question (design a scalable solution)\n\n" |
| "✅ Pass all 3 levels with a **70%+ average** to earn a verified badge!\n" |
| "🏆 Get **80%+ of your skills verified** to unlock the profile verified badge." |
| ) |
| }, |
| |
| { |
| 'keywords': ['view applicant', 'manage applicant', 'candidate', 'application', 'applicant list', 'review applicant', 'applicant status', 'hirer applicant', 'see applicants'], |
| 'reply': ( |
| "To view and manage applicants:\n\n" |
| "1️⃣ Go to your **Hirer Dashboard**\n" |
| "2️⃣ Scroll to the **Applicants** section\n" |
| "3️⃣ For each applicant you can:\n" |
| " • 👁️ View their profile, skills, and experience\n" |
| " • 📧 **Email** them directly\n" |
| " • 📄 **Download** their resume\n" |
| " • 🔄 Change status: Applied → Interview → Offer → Rejected\n" |
| "4️⃣ Use the **Verified only** toggle to see candidates who passed AI verification\n\n" |
| "The match score shows how well their skills align with the job requirements." |
| ) |
| }, |
| |
| { |
| 'keywords': ['trending skill', 'popular skill', 'skill trend', 'in demand skill', 'market trend', 'hot skill', 'trending', 'what skills are needed', 'skills most paying', 'most paying skill', 'highest paying skill', 'best paying skill', 'most paying jobs', 'highest paying jobs', 'best paying jobs', 'top paying jobs', 'highest salary', 'most paying', 'best paying', 'top paying', 'what pays well', 'salary skill', 'salary range', 'paying job', 'highest paying job', 'best paying tech', 'most valuable skill', 'skill salary'], |
| 'reply': ( |
| "Here are the most in-demand and highest-paying skills on Nexus ATS right now:\n\n" |
| "💰 **Top Paying Skills**:\n" |
| "• 🧠 **Python** — $125K avg (Data, AI, Backend)\n" |
| "• ⚛️ **React** — $115K avg (Frontend, Full-Stack)\n" |
| "• ☁️ **AWS** — $130K avg (Cloud, DevOps)\n" |
| "• 📘 **TypeScript** — $120K avg (Frontend, Full-Stack)\n" |
| "• 🟢 **Node.js** — $110K avg (Backend, APIs)\n" |
| "• 🐳 **Docker** — $125K avg (DevOps, Infra)\n\n" |
| "📊 Check the **Trending Skills** section on your Seeker Dashboard for real-time data from all active job postings on the platform!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['languages required', 'languages for', 'languages do i need', 'what language', 'languages needed', 'required for react', 'skills for python', 'skills required', 'what do i need', 'prerequisites', 'before learning', 'tech stack', 'what to learn', 'how to learn', 'should i learn', 'which language', 'what technologies', 'framework for', 'tools for'], |
| 'reply': ( |
| "Great question! Here's a quick guide:\n\n" |
| "⚛️ **React**: JavaScript (or TypeScript), HTML/CSS, JSX\n" |
| "🐍 **Python**: Python basics, then frameworks like Django/Flask/FastAPI\n" |
| "🟢 **Node.js**: JavaScript, npm, Express.js\n" |
| "☁️ **AWS/Cloud**: Linux basics, networking, one scripting language\n" |
| "🐳 **Docker/DevOps**: YAML, Bash/Shell, Linux\n" |
| "📊 **Data Science**: Python, SQL, Pandas, NumPy\n" |
| "🧠 **ML/AI**: Python, linear algebra, statistics\n\n" |
| "💡 Switch to **AI mode** for detailed learning paths and personalized advice!" |
| ) |
| }, |
| { |
| 'keywords': ['profile', 'account', 'setting', 'edit profile', 'change username', 'update profile', 'my info'], |
| 'reply': ( |
| "To manage your profile:\n\n" |
| "1️⃣ Click your **username** in the top navigation bar\n" |
| "2️⃣ Select **Profile** from the dropdown\n" |
| "3️⃣ You can update:\n" |
| " • Personal info, experience, education\n" |
| " • Social links (LinkedIn, GitHub, portfolio)\n" |
| " • Company info (if you're a hirer)\n" |
| "4️⃣ Click **Save changes** to update\n\n" |
| "Your email cannot be changed (it's your login identifier)." |
| ) |
| }, |
| |
| { |
| 'keywords': ['resume', 'upload resume', 'upload cv', 'resume parsing', 'parse resume', 'cv upload', 'upload my resume'], |
| 'reply': ( |
| "To upload your resume:\n\n" |
| "1️⃣ Go to your **Seeker Dashboard**\n" |
| "2️⃣ Click the **upload area** or drag & drop your file\n" |
| "3️⃣ Supported formats: **PDF** and **DOCX**\n" |
| "4️⃣ Max file size: **5MB**\n\n" |
| "Our AI will parse your resume and extract:\n" |
| "• Your skills (300+ skill keywords supported)\n" |
| "• Experience, education, certifications\n" |
| "• Contact info and social links\n\n" |
| "After parsing, click on skill pills to start the **3-level verification interview**!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['match score', 'matching', 'how match work', 'score calculated', 'job match', 'match percentage'], |
| 'reply': ( |
| "Match scores show how well your skills align with a job's requirements:\n\n" |
| "📊 **Score breakdown**:\n" |
| "• **80-100%** (green) — Excellent match, highly recommended\n" |
| "• **60-79%** (yellow) — Good match, some skill gaps\n" |
| "• **0-59%** (red) — Low match, consider upskilling\n\n" |
| "💡 **Tips to improve your match score**:\n" |
| "• Verify more skills through the 3-level interview\n" |
| "• Upload an updated resume with all your skills\n" |
| "• Focus on trending/in-demand skills in your field" |
| ) |
| }, |
| |
| { |
| 'keywords': ['what is nexus', 'about nexus', 'what is ats', 'platform info', 'about this', 'tell me about', 'nexus ats', 'what is this platform'], |
| 'reply': ( |
| "**Nexus ATS** is an AI-powered Applicant Tracking System and job marketplace.\n\n" |
| "✨ **Key features**:\n" |
| "• 🧠 **Deep skill verification** — 3-level AI interviews per skill\n" |
| "• 📊 **Trending skill insights** — Real-time market demand data\n" |
| "• 📄 **Smart resume parsing** — 300+ skill keyword extraction\n" |
| "• 🎯 **Match scoring** — Skills-based job matching\n" |
| "• 🎨 **3D-enhanced UI** — Modern, professional interface\n" |
| "• 💬 **Nexus Assistant** — AI chatbot for platform help\n\n" |
| "Built for modern, skills-first hiring where what you can do matters more than your job title." |
| ) |
| }, |
| |
| { |
| 'keywords': ['email applicant', 'send email', 'contact applicant', 'email candidate', 'message applicant', 'reach out'], |
| 'reply': ( |
| "To email an applicant:\n\n" |
| "1️⃣ Go to your **Hirer Dashboard**\n" |
| "2️⃣ Find the applicant in the **Applicants** table\n" |
| "3️⃣ Click the **Email** button next to their name\n" |
| "4️⃣ A compose window will open where you can write a subject and message\n\n" |
| "If SMTP email is not configured, the button will open your default email client instead." |
| ) |
| }, |
| |
| { |
| 'keywords': ['status', 'application status', 'change status', 'interview', 'offer', 'reject', 'hire candidate'], |
| 'reply': ( |
| "To update an applicant's status:\n\n" |
| "1️⃣ Go to your **Hirer Dashboard**\n" |
| "2️⃣ Find the applicant in the **Applicants** table\n" |
| "3️⃣ Use the **status dropdown** next to their name\n" |
| "4️⃣ Available statuses:\n" |
| " • **Applied** — Initial state after applying\n" |
| " • **Interview** — Selected for interview\n" |
| " • **Offer** — Job offer extended\n" |
| " • **Rejected** — Not moving forward\n\n" |
| "Notifications will be sent to seekers when their status changes!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['help', 'guide', 'tutorial', 'how to use', 'instructions', 'walkthrough', 'show me around'], |
| 'reply': ( |
| "Here's what I can help you with:\n\n" |
| "🔍 **Finding jobs** — Browse open positions on your seeker dashboard\n" |
| "✅ **Skill verification** — Upload a resume, answer AI questions, earn verified badges\n" |
| "📝 **Posting jobs** — Hirers can create listings from the hirer dashboard\n" |
| "👥 **Viewing applicants** — Manage candidates, change status, filter by verification\n" |
| "🏆 **Profile verification** — Get 80%+ skills verified for a verified badge\n" |
| "📊 **Trending skills** — See what skills employers are looking for\n" |
| "📧 **Email applicants** — Contact candidates directly from the platform\n\n" |
| "Just ask me anything about these topics!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['logout', 'sign out', 'signout', 'log out'], |
| 'reply': "To log out, click your **username** in the top navigation bar and select **Logout** from the dropdown. You'll be redirected to the home page." |
| }, |
| |
| { |
| 'keywords': ['register', 'sign up', 'create account', 'new account', 'join', 'how to join'], |
| 'reply': ( |
| "To create a new account:\n\n" |
| "1️⃣ Click **Sign In** in the navigation bar\n" |
| "2️⃣ Click **Create one** to open registration\n" |
| "3️⃣ Fill in your name, email, and password\n" |
| "4️⃣ Choose your role: **Seeker** (find jobs) or **Hirer** (post jobs)\n" |
| "5️⃣ Hit **Create Account**\n\n" |
| "You'll be automatically logged in and redirected to your dashboard!" |
| ) |
| }, |
| |
| { |
| 'keywords': ['dashboard', 'go to dashboard', 'my dashboard'], |
| 'reply': ( |
| "You can reach your dashboard by:\n" |
| "1️⃣ Clicking your **username** in the navigation bar\n" |
| "2️⃣ Selecting **Dashboard** from the dropdown\n\n" |
| "Seekers see job listings, skill verification, and trending skills.\n" |
| "Hirers see job posting forms, their listings, and applicant management." |
| ) |
| }, |
| |
| { |
| 'keywords': ['weather', 'news', 'sports', 'music', 'movie', 'film', 'game', 'gaming', 'recipe', 'cook'], |
| 'reply': "I appreciate the interest, but I'm focused on helping with Nexus ATS — the job marketplace and skill verification platform! Ask me about finding jobs, verifying skills, or managing applicants. 😊" |
| }, |
| { |
| 'keywords': ['politics', 'religion', 'crypto', 'bitcoin', 'stock', 'investment', 'dating'], |
| 'reply': "I'm not the best source for that — I'm here to help with Nexus ATS! Try asking about job searching, skill verification, or posting jobs instead." |
| }, |
| ] |
|
|
| |
| CHATBOT_DEFAULT = ( |
| "I'm not sure about that, but I can help with:\n\n" |
| "🔧 **Platform**: Find jobs, verify skills, post listings, manage applicants\n" |
| "📚 **Learn**: Python, JavaScript, React, SQL, Docker, Git, and more\n" |
| "🧮 **Math**: Try \"what is 2+2\" or \"calculate 15*7\"\n" |
| "💼 **Career**: Salary info, skill requirements, learning paths\n\n" |
| "Just ask — I'm here to help!" |
| ) |
|
|
| def get_chatbot_reply(message, user=None): |
| """Match user message to best topic using phrase/substring matching only.""" |
| msg_lower = message.lower().strip() |
| |
| |
| exact_greetings = {'hi', 'hello', 'hey', 'yo', 'hiya', 'howdy', 'sup'} |
| if msg_lower in exact_greetings or msg_lower in ['good morning', 'good evening', 'good afternoon']: |
| for topic in CHATBOT_TOPICS: |
| if msg_lower in [k.lower() for k in topic['keywords']]: |
| return topic['reply'] |
| |
| |
| all_topics = CHATBOT_TOPICS + GENERAL_KNOWLEDGE |
| best_score = 0 |
| best_reply = None |
| |
| for topic in all_topics: |
| keyword_score = 0 |
| for keyword in topic['keywords']: |
| kw = keyword.lower() |
| if kw == msg_lower: |
| keyword_score += 10 |
| elif re.search(r'\b' + re.escape(kw), msg_lower): |
| keyword_score += 3 + len(kw.split()) |
| elif re.search(r'\b' + re.escape(msg_lower), kw): |
| keyword_score += 2 |
| |
| if keyword_score > best_score: |
| best_score = keyword_score |
| best_reply = topic['reply'] |
| |
| |
| if user and best_reply: |
| if user.get('role') == 'hirer': |
| if any(w in msg_lower for w in ['my job', 'my listing', 'my applicant']): |
| best_reply += "\n\n💡 You're logged in as a hirer — check your Hirer Dashboard to manage your jobs and applicants!" |
| elif user.get('role') == 'seeker': |
| if any(w in msg_lower for w in ['apply', 'find job', 'search']): |
| best_reply += "\n\n💡 You're logged in as a seeker — go to your Seeker Dashboard to browse and apply for jobs!" |
| |
| return best_reply |
|
|
| @app.route('/api/chatbot', methods=['POST']) |
| def chatbot(): |
| try: |
| data = request.json or {} |
| message = data.get('message', '').strip() |
| mode = data.get('mode', 'rule') |
| |
| if not message: |
| return jsonify({'success': True, 'reply': CHATBOT_DEFAULT}) |
| |
| user = get_current_user() |
| chat_history = session.get('chat_history', []) |
| |
| def _save_and_return(reply, reply_mode): |
| if 'chat_history' not in session: |
| session['chat_history'] = [] |
| session['chat_history'].append({'q': message, 'a': reply}) |
| session['chat_history'] = session['chat_history'][-5:] |
| return jsonify({'success': True, 'reply': reply, 'mode': reply_mode}) |
| |
| |
| math_answer = _try_math_answer(message) |
| if math_answer: |
| return _save_and_return(math_answer, 'rule') |
| |
| |
| if mode == 'ai': |
| reply, status = generate_llm_chat(message, chat_history) |
| if reply: |
| return _save_and_return(reply, 'ai') |
| |
| |
| |
| reply = get_chatbot_reply(message, user) |
| if reply and reply != CHATBOT_DEFAULT: |
| return _save_and_return(reply, 'rule') |
| |
| |
| ai_reply, status = generate_llm_chat(message, chat_history) |
| if ai_reply: |
| return _save_and_return(ai_reply, 'ai') |
| |
| |
| return _save_and_return(CHATBOT_DEFAULT, 'rule') |
| except Exception as e: |
| print(f" [CHAT] Error: {e}") |
| return jsonify({'success': True, 'reply': CHATBOT_DEFAULT, 'mode': 'rule'}) |
|
|
| @app.route('/api/chatbot-models', methods=['GET']) |
| def chatbot_models(): |
| try: |
| from ai_pipeline import is_llm_ready |
| ai_ready = is_llm_ready() |
| except Exception: |
| ai_ready = False |
| return jsonify({ |
| 'success': True, |
| 'ai': { |
| 'available': True, |
| 'ready': True, |
| 'loading': False, |
| 'model': 'AI-powered (cloud)', |
| 'size': 'free, instant' |
| }, |
| 'rule': {'available': True, 'model': 'built-in rules'} |
| }) |
|
|
| |
| @app.route('/api/send-email', methods=['POST']) |
| @login_required |
| def send_email(): |
| user = get_current_user() |
| data = request.json or {} |
| recipient = data.get('to', '').strip() |
| subject = data.get('subject', '').strip() |
| body = data.get('body', '').strip() |
| |
| if not recipient or not subject or not body: |
| return jsonify({'success': False, 'message': 'Missing recipient, subject, or body'}), 400 |
| |
| |
| if not app.config['SMTP_SERVER'] or not app.config['SMTP_USERNAME']: |
| return jsonify({ |
| 'success': False, |
| 'message': 'SMTP email is not configured. Please set SMTP_SERVER, SMTP_USERNAME, and SMTP_PASSWORD environment variables to enable email sending.', |
| 'fallback_mailto': f'mailto:{recipient}?subject={subject}&body={body}' |
| }), 503 |
| |
| try: |
| msg = EmailMessage() |
| msg.set_content(body) |
| msg['Subject'] = subject |
| msg['From'] = app.config['MAIL_FROM'] |
| msg['To'] = recipient |
| |
| server = smtplib.SMTP(app.config['SMTP_SERVER'], app.config['SMTP_PORT']) |
| server.starttls() |
| server.login(app.config['SMTP_USERNAME'], app.config['SMTP_PASSWORD']) |
| server.send_message(msg) |
| server.quit() |
| |
| |
| conn = get_db() |
| conn.execute("INSERT INTO sent_emails (from_user_id, recipient, subject) VALUES (?,?,?)", |
| (user['id'], recipient, subject)) |
| conn.commit() |
| conn.close() |
| |
| return jsonify({'success': True, 'message': 'Email sent successfully'}) |
| except Exception as e: |
| return jsonify({'success': False, 'message': f'Failed to send email: {str(e)}'}), 500 |
|
|
| @app.route('/health') |
| def health(): |
| return jsonify({'status': 'ok', 'db': 'sqlite', 'db_path': DB_PATH}) |
|
|
| |
| @socketio.on('connect') |
| def handle_connect(): |
| emit('status', {'msg': 'Connected'}) |
|
|
| @socketio.on('disconnect') |
| def handle_disconnect(): |
| pass |
|
|
| if __name__ == '__main__': |
| print(f" DB: {DB_PATH}") |
| print(f" Uploads: {app.config['UPLOAD_FOLDER']}/") |
| print(f" Open: http://localhost:7860") |
| print(f" Demo: seeker@test.com / password123") |
| print(f" Demo: hirer@test.com / password123") |
| socketio.run(app, debug=True, host='0.0.0.0', port=7860) |
|
|