fix(Matcher): Raw text role auto-detection and domain filter pills
Browse files- deployment/frontend/app.js +123 -5
deployment/frontend/app.js
CHANGED
|
@@ -70,6 +70,76 @@ function ScoreRadialGauge({ score = 0, fitTier = "Good Fit" }) {
|
|
| 70 |
);
|
| 71 |
}
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
function App() {
|
| 74 |
const [currentPage, setCurrentPage] = useState("search"); // 'search' (Page 1) | 'matcher' (Page 2)
|
| 75 |
const [sampleData, setSampleData] = useState({ personas: [], jobs: [] });
|
|
@@ -646,18 +716,32 @@ function App() {
|
|
| 646 |
style={{ width: '100%', height: '140px', padding: '10px', border: '1px solid #cbd5e1', borderRadius: '6px', fontFamily: 'monospace', fontSize: '0.82rem', outline: 'none' }}
|
| 647 |
value={resumeText}
|
| 648 |
onChange={(e) => setResumeText(e.target.value)}
|
| 649 |
-
placeholder="Paste raw CV text..."
|
| 650 |
/>
|
| 651 |
<button
|
| 652 |
className="search-submit-btn"
|
| 653 |
-
style={{ marginTop: '8px', padding: '6px
|
| 654 |
onClick={() => {
|
| 655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
setShowTextPaste(false);
|
| 657 |
-
|
|
|
|
|
|
|
| 658 |
}}
|
| 659 |
>
|
| 660 |
-
Re-Analyze
|
| 661 |
</button>
|
| 662 |
</div>
|
| 663 |
)}
|
|
@@ -692,6 +776,40 @@ function App() {
|
|
| 692 |
</div>
|
| 693 |
</div>
|
| 694 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
{loading ? (
|
| 696 |
<div className="jobs-list-container">
|
| 697 |
{[1, 2, 3].map(i => (
|
|
|
|
| 70 |
);
|
| 71 |
}
|
| 72 |
|
| 73 |
+
// 🎯 Client-Side Candidate Role & Name Extraction
|
| 74 |
+
const KNOWN_ROLES_LIST = [
|
| 75 |
+
'Full Stack Developer', 'Full Stack Engineer',
|
| 76 |
+
'Frontend Developer', 'Frontend Engineer', 'UI Developer',
|
| 77 |
+
'Backend Developer', 'Backend Engineer',
|
| 78 |
+
'Machine Learning Engineer', 'AI Engineer', 'NLP Engineer', 'Deep Learning Engineer',
|
| 79 |
+
'Data Scientist', 'Data Analyst', 'Data Engineer',
|
| 80 |
+
'DevOps Engineer', 'Cloud Engineer', 'Site Reliability Engineer',
|
| 81 |
+
'Software Engineer', 'Software Developer',
|
| 82 |
+
'QA Engineer', 'Mobile Developer', 'Android Developer', 'iOS Developer',
|
| 83 |
+
'HR Specialist', 'HR Manager', 'Talent Acquisition Specialist',
|
| 84 |
+
'Product Manager', 'Project Manager', 'UI/UX Designer'
|
| 85 |
+
];
|
| 86 |
+
|
| 87 |
+
function detectRoleFromClientText(cleanText) {
|
| 88 |
+
if (!cleanText) return "Software Engineer";
|
| 89 |
+
const lines = cleanText.split('\n').map(l => l.trim()).filter(Boolean);
|
| 90 |
+
|
| 91 |
+
// 1. Check top 8 lines
|
| 92 |
+
for (let i = 0; i < Math.min(lines.length, 8); i++) {
|
| 93 |
+
for (let role of KNOWN_ROLES_LIST) {
|
| 94 |
+
const re = new RegExp('\\b' + role.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i');
|
| 95 |
+
if (re.test(lines[i])) {
|
| 96 |
+
return role;
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// 2. Keyword heuristic fallback
|
| 102 |
+
const t = cleanText.toLowerCase();
|
| 103 |
+
if (t.includes('devops') || t.includes('kubernetes') || t.includes('docker') || t.includes('terraform') || t.includes('ci/cd') || t.includes('jenkins') || t.includes('ansible')) {
|
| 104 |
+
return 'DevOps Engineer';
|
| 105 |
+
}
|
| 106 |
+
if (t.includes('full stack') || (t.includes('react') && (t.includes('node') || t.includes('express') || t.includes('django')))) {
|
| 107 |
+
return 'Full Stack Developer';
|
| 108 |
+
}
|
| 109 |
+
if (t.includes('frontend') || t.includes('react') || t.includes('vue') || t.includes('angular') || t.includes('css3') || t.includes('html5')) {
|
| 110 |
+
return 'Frontend Developer';
|
| 111 |
+
}
|
| 112 |
+
if (t.includes('backend') || t.includes('fastapi') || t.includes('django') || t.includes('spring boot') || t.includes('express.js')) {
|
| 113 |
+
return 'Backend Developer';
|
| 114 |
+
}
|
| 115 |
+
if (t.includes('machine learning') || t.includes('pytorch') || t.includes('deep learning') || t.includes('tensorflow') || t.includes('computer vision') || t.includes('nlp')) {
|
| 116 |
+
return 'Machine Learning Engineer';
|
| 117 |
+
}
|
| 118 |
+
if (t.includes('data scientist')) {
|
| 119 |
+
return 'Data Scientist';
|
| 120 |
+
}
|
| 121 |
+
if (t.includes('data analyst') || t.includes('tableau') || t.includes('power bi')) {
|
| 122 |
+
return 'Data Analyst';
|
| 123 |
+
}
|
| 124 |
+
if (t.includes('hr') || t.includes('human resource') || t.includes('recruitment') || t.includes('talent acquisition')) {
|
| 125 |
+
return 'HR Specialist';
|
| 126 |
+
}
|
| 127 |
+
return 'Software Engineer';
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function detectNameFromClientText(cleanText) {
|
| 131 |
+
if (!cleanText) return "Candidate";
|
| 132 |
+
const lines = cleanText.split('\n').map(l => l.trim()).filter(Boolean);
|
| 133 |
+
for (let line of lines.slice(0, 3)) {
|
| 134 |
+
if (!/resume|curriculum|vitae|summary|experience|education|contact|phone|email|profile/i.test(line)) {
|
| 135 |
+
if (line.split(/\s+/).length <= 4 && line.length <= 40) {
|
| 136 |
+
return line.replace(/\|/g, '').trim();
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
return "Candidate";
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
function App() {
|
| 144 |
const [currentPage, setCurrentPage] = useState("search"); // 'search' (Page 1) | 'matcher' (Page 2)
|
| 145 |
const [sampleData, setSampleData] = useState({ personas: [], jobs: [] });
|
|
|
|
| 716 |
style={{ width: '100%', height: '140px', padding: '10px', border: '1px solid #cbd5e1', borderRadius: '6px', fontFamily: 'monospace', fontSize: '0.82rem', outline: 'none' }}
|
| 717 |
value={resumeText}
|
| 718 |
onChange={(e) => setResumeText(e.target.value)}
|
| 719 |
+
placeholder="Paste raw CV text (e.g. DevOps Engineer with Kubernetes, Docker, CI/CD, Terraform, AWS)..."
|
| 720 |
/>
|
| 721 |
<button
|
| 722 |
className="search-submit-btn"
|
| 723 |
+
style={{ marginTop: '8px', padding: '6px 16px', fontSize: '0.82rem' }}
|
| 724 |
onClick={() => {
|
| 725 |
+
const wordCount = resumeText.split(/\s+/).filter(Boolean).length;
|
| 726 |
+
setUploadedWordCount(wordCount);
|
| 727 |
+
|
| 728 |
+
// Detect candidate role and name from raw pasted text
|
| 729 |
+
const detectedRole = detectRoleFromClientText(resumeText);
|
| 730 |
+
const detectedName = detectNameFromClientText(resumeText);
|
| 731 |
+
|
| 732 |
+
if (detectedName && detectedName !== "Candidate") {
|
| 733 |
+
setUserName(detectedName);
|
| 734 |
+
}
|
| 735 |
+
setUserRole(detectedRole);
|
| 736 |
+
setSearchQuery(detectedRole);
|
| 737 |
+
setUploadedFileName("Pasted Resume Document");
|
| 738 |
setShowTextPaste(false);
|
| 739 |
+
|
| 740 |
+
// Automatically search and match jobs for detected role
|
| 741 |
+
fetchJobsAndMatch(resumeText, detectedRole, searchLocation);
|
| 742 |
}}
|
| 743 |
>
|
| 744 |
+
🔍 Auto-Detect Role & Re-Analyze Matches
|
| 745 |
</button>
|
| 746 |
</div>
|
| 747 |
)}
|
|
|
|
| 776 |
</div>
|
| 777 |
</div>
|
| 778 |
|
| 779 |
+
{/* Domain Filter Pills on Matcher Page */}
|
| 780 |
+
<div style={{ display: 'flex', gap: '5px', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
| 781 |
+
{[
|
| 782 |
+
{ label: `🎯 ${userRole.split(' ')[0]} ${userRole.split(' ')[1] || ''}`, query: userRole },
|
| 783 |
+
{ label: "☁️ DevOps", query: "DevOps Engineer" },
|
| 784 |
+
{ label: "⚡ Full Stack", query: "Full Stack Developer" },
|
| 785 |
+
{ label: "⚛️ Frontend", query: "Frontend Developer" },
|
| 786 |
+
{ label: "🐍 Backend", query: "Backend Developer" },
|
| 787 |
+
{ label: "🤖 AI & ML", query: "AI Engineer" },
|
| 788 |
+
{ label: "📊 Data Analyst", query: "Data Analyst" }
|
| 789 |
+
].map((tab) => (
|
| 790 |
+
<button
|
| 791 |
+
key={tab.label}
|
| 792 |
+
style={{
|
| 793 |
+
padding: '4px 10px',
|
| 794 |
+
fontSize: '0.75rem',
|
| 795 |
+
fontWeight: '700',
|
| 796 |
+
background: searchQuery.toLowerCase().includes(tab.query.toLowerCase().split(' ')[0]) ? '#0284c7' : '#ffffff',
|
| 797 |
+
color: searchQuery.toLowerCase().includes(tab.query.toLowerCase().split(' ')[0]) ? '#ffffff' : '#475569',
|
| 798 |
+
border: '1px solid #cbd5e1',
|
| 799 |
+
borderRadius: '999px',
|
| 800 |
+
cursor: 'pointer',
|
| 801 |
+
transition: 'all 0.15s ease'
|
| 802 |
+
}}
|
| 803 |
+
onClick={() => {
|
| 804 |
+
setSearchQuery(tab.query);
|
| 805 |
+
fetchJobsAndMatch(resumeText, tab.query, searchLocation);
|
| 806 |
+
}}
|
| 807 |
+
>
|
| 808 |
+
{tab.label}
|
| 809 |
+
</button>
|
| 810 |
+
))}
|
| 811 |
+
</div>
|
| 812 |
+
|
| 813 |
{loading ? (
|
| 814 |
<div className="jobs-list-container">
|
| 815 |
{[1, 2, 3].map(i => (
|