Adeen
Prepare for Hugging Face deployment
c7fb8cf
/**
* JobForm.jsx
* The main HR input form: job details, filters, and drag-and-drop resume upload.
*/
import { useState, useRef, useCallback } from 'react';
import {
Briefcase, FileText, Star, Clock, GraduationCap, Award,
MapPin, Globe, Upload, X, ChevronDown, ChevronUp, Zap, Users
} from 'lucide-react';
export default function JobForm({ onSubmit, loading }) {
// ── Job Details ─────────────────────────────────────────────────────────
const [jobTitle, setJobTitle] = useState('');
const [jobDescription, setJobDescription] = useState('');
const [requiredSkills, setRequiredSkills] = useState('');
const [experienceYears, setExperienceYears] = useState('');
const [education, setEducation] = useState('');
const [certifications, setCertifications] = useState('');
const [topN, setTopN] = useState(10);
// ── Filters ──────────────────────────────────────────────────────────────
const [preferredLocation, setPreferredLocation] = useState('');
const [preferredLanguages, setPreferredLanguages] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
// ── Resume Upload ────────────────────────────────────────────────────────
const [files, setFiles] = useState([]);
const [resumeText, setResumeText] = useState('');
const [dragging, setDragging] = useState(false);
const fileInputRef = useRef(null);
// ── Drag and Drop handlers ───────────────────────────────────────────────
const handleDrop = useCallback((e) => {
e.preventDefault();
setDragging(false);
const dropped = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf');
setFiles(prev => {
const names = new Set(prev.map(f => f.name));
return [...prev, ...dropped.filter(f => !names.has(f.name))];
});
}, []);
const handleDragOver = (e) => { e.preventDefault(); setDragging(true); };
const handleDragLeave = () => setDragging(false);
const handleFileChange = (e) => {
const selected = Array.from(e.target.files).filter(f => f.type === 'application/pdf');
setFiles(prev => {
const names = new Set(prev.map(f => f.name));
return [...prev, ...selected.filter(f => !names.has(f.name))];
});
e.target.value = '';
};
const removeFile = (name) => setFiles(prev => prev.filter(f => f.name !== name));
// ── Form submit ──────────────────────────────────────────────────────────
const handleSubmit = (e) => {
e.preventDefault();
if (!files.length && !resumeText.trim()) {
alert('Please upload at least one resume PDF or paste resume text.');
return;
}
const formData = new FormData();
formData.append('job_title', jobTitle);
formData.append('job_description', jobDescription);
formData.append('required_skills', requiredSkills);
formData.append('experience_years', experienceYears || '0');
formData.append('education', education);
formData.append('certifications', certifications);
formData.append('top_n', topN);
formData.append('preferred_location', preferredLocation);
formData.append('preferred_languages', preferredLanguages);
formData.append('resume_text', resumeText);
// Append all PDF files
if (files.length > 0) {
files.forEach(f => formData.append('resumes', f));
} else {
// Append a blank file so FastAPI 'resumes' field isn't empty
formData.append('resumes', new Blob([]), 'placeholder.pdf');
}
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit} className="space-y-6 animate-fade-in">
{/* ── Header ── */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">New Screening</h2>
<p className="text-slate-400 text-sm mt-1">Configure job requirements and upload candidate resumes</p>
</div>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary-900/30 border border-primary-800/40">
<Zap size={14} className="text-primary-400" />
<span className="text-xs text-primary-300 font-medium">AI-Powered</span>
</div>
</div>
{/* ── Job Details Card ── */}
<div className="card p-6 space-y-5">
<h3 className="section-header">
<Briefcase size={18} className="text-primary-400" />
Job Details
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Job Title */}
<div>
<label className="label" htmlFor="job-title">Job Title *</label>
<input
id="job-title"
className="input-field"
placeholder="e.g. Senior Software Engineer"
value={jobTitle}
onChange={e => setJobTitle(e.target.value)}
required
/>
</div>
{/* Top N */}
<div>
<label className="label" htmlFor="top-n">
<Users size={14} className="inline mr-1.5" />
Top N Candidates
</label>
<input
id="top-n"
type="number"
min={1}
max={100}
className="input-field"
value={topN}
onChange={e => setTopN(parseInt(e.target.value) || 10)}
/>
</div>
</div>
{/* Job Description */}
<div>
<label className="label" htmlFor="job-description">
<FileText size={14} className="inline mr-1.5" />
Job Description *
</label>
<textarea
id="job-description"
className="input-field min-h-[120px] resize-y"
placeholder="Paste the full job description here..."
value={jobDescription}
onChange={e => setJobDescription(e.target.value)}
required
/>
</div>
{/* Required Skills */}
<div>
<label className="label" htmlFor="required-skills">
<Star size={14} className="inline mr-1.5" />
Required Skills *
<span className="text-slate-600 font-normal ml-2">(comma separated)</span>
</label>
<input
id="required-skills"
className="input-field"
placeholder="e.g. Python, React, SQL, Machine Learning"
value={requiredSkills}
onChange={e => setRequiredSkills(e.target.value)}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{/* Experience */}
<div>
<label className="label" htmlFor="experience-years">
<Clock size={14} className="inline mr-1.5" />
Experience (years)
</label>
<input
id="experience-years"
type="number"
min={0}
className="input-field"
placeholder="e.g. 3"
value={experienceYears}
onChange={e => setExperienceYears(e.target.value)}
/>
</div>
{/* Education */}
<div>
<label className="label" htmlFor="education">
<GraduationCap size={14} className="inline mr-1.5" />
Required Education
</label>
<select
id="education"
className="input-field"
value={education}
onChange={e => setEducation(e.target.value)}
>
<option value="">Any</option>
<option value="certificate">Certificate / Diploma</option>
<option value="bachelor">Bachelor's Degree</option>
<option value="master">Master's Degree</option>
<option value="phd">PhD / Doctorate</option>
</select>
</div>
{/* Certifications */}
<div>
<label className="label" htmlFor="certifications">
<Award size={14} className="inline mr-1.5" />
Required Certifications
</label>
<input
id="certifications"
className="input-field"
placeholder="e.g. AWS Certified, PMP"
value={certifications}
onChange={e => setCertifications(e.target.value)}
/>
</div>
</div>
</div>
{/* ── Optional Filters ── */}
<div className="card overflow-hidden">
<button
type="button"
onClick={() => setFiltersOpen(v => !v)}
className="w-full flex items-center justify-between p-5 hover:bg-slate-800/50 transition-colors"
>
<h3 className="section-header">
<MapPin size={18} className="text-primary-400" />
Filters &amp; Bonus Factors
<span className="ml-2 text-xs text-slate-500 font-normal">(5% weight)</span>
</h3>
{filtersOpen ? <ChevronUp size={18} className="text-slate-500" /> : <ChevronDown size={18} className="text-slate-500" />}
</button>
{filtersOpen && (
<div className="px-6 pb-6 grid grid-cols-1 md:grid-cols-2 gap-5 border-t border-slate-800 pt-5 animate-fade-in">
<div>
<label className="label" htmlFor="preferred-location">
<MapPin size={14} className="inline mr-1.5" />
Preferred Location
</label>
<input
id="preferred-location"
className="input-field"
placeholder="e.g. Karachi, Pakistan"
value={preferredLocation}
onChange={e => setPreferredLocation(e.target.value)}
/>
</div>
<div>
<label className="label" htmlFor="preferred-languages">
<Globe size={14} className="inline mr-1.5" />
Preferred Languages
<span className="text-slate-600 font-normal ml-1">(comma separated)</span>
</label>
<input
id="preferred-languages"
className="input-field"
placeholder="e.g. English, Urdu"
value={preferredLanguages}
onChange={e => setPreferredLanguages(e.target.value)}
/>
</div>
</div>
)}
</div>
{/* ── Resume Upload Card ── */}
<div className="card p-6 space-y-5">
<h3 className="section-header">
<Upload size={18} className="text-primary-400" />
Resume Upload
</h3>
{/* Drop Zone */}
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => fileInputRef.current.click()}
className={`border-2 border-dashed rounded-xl p-10 text-center cursor-pointer transition-all duration-300 ${
dragging
? 'border-primary-500 bg-primary-900/20 drop-active'
: 'border-slate-700 hover:border-slate-600 hover:bg-slate-800/30'
}`}
>
<input
ref={fileInputRef}
type="file"
id="resume-upload"
multiple
accept=".pdf"
className="hidden"
onChange={handleFileChange}
/>
<div className="flex flex-col items-center gap-3">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center transition-colors ${
dragging ? 'bg-primary-600/30' : 'bg-slate-800'
}`}>
<Upload size={26} className={dragging ? 'text-primary-400' : 'text-slate-500'} />
</div>
<div>
<p className="text-slate-300 font-medium">
{dragging ? 'Drop PDFs here!' : 'Drag & drop PDF resumes'}
</p>
<p className="text-slate-500 text-sm mt-1">or click to browse Β· Multiple files supported</p>
</div>
</div>
</div>
{/* File list */}
{files.length > 0 && (
<div className="space-y-2 animate-fade-in">
<p className="text-slate-400 text-sm font-medium">{files.length} file(s) selected</p>
<div className="max-h-48 overflow-y-auto space-y-1.5 pr-1">
{files.map(f => (
<div
key={f.name}
className="flex items-center justify-between px-4 py-2.5 bg-slate-800 rounded-lg border border-slate-700"
>
<div className="flex items-center gap-2.5 min-w-0">
<FileText size={16} className="text-primary-400 flex-shrink-0" />
<span className="text-sm text-slate-300 truncate">{f.name}</span>
<span className="text-xs text-slate-600 flex-shrink-0">
{(f.size / 1024).toFixed(0)} KB
</span>
</div>
<button
type="button"
onClick={(e) => { e.stopPropagation(); removeFile(f.name); }}
className="ml-2 text-slate-600 hover:text-red-400 transition-colors flex-shrink-0"
>
<X size={16} />
</button>
</div>
))}
</div>
</div>
)}
{/* Text paste option */}
<div>
<label className="label" htmlFor="resume-text">
Or Paste Resume Text (optional)
</label>
<textarea
id="resume-text"
className="input-field min-h-[80px] resize-y"
placeholder="Paste a plain-text resume here as an alternative or addition..."
value={resumeText}
onChange={e => setResumeText(e.target.value)}
/>
</div>
</div>
{/* ── Submit Button ── */}
<button
id="run-screening-btn"
type="submit"
disabled={loading}
className="btn-primary w-full flex items-center justify-center gap-2.5 py-4 text-base"
>
{loading ? (
<>
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" />
</svg>
Processing Resumes...
</>
) : (
<>
<Zap size={20} />
Run AI Screening
</>
)}
</button>
</form>
);
}