resume-parser / components /resume-match-view.tsx
PPSA's picture
Upload 235 files
dca8ede verified
"use client"
import { useState } from "react"
import type { Resume } from "@/types/resume"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Progress } from "@/components/ui/progress"
import { JobDescriptionSelector } from "@/components/job-description-selector"
import { toast } from "@/components/ui/use-toast"
import { CheckCircle2, XCircle, AlertCircle } from "lucide-react"
interface ResumeMatchViewProps {
resume: Resume
onStatusChange: (status: string) => void
}
interface JobDescription {
id: string
title: string
description: string
}
interface MatchResult {
matchScore: number
matchedSkills: string[]
missingSkills: string[]
experienceMatch: number
educationMatch: number
overallAssessment: string
recommendations: string[]
}
export function ResumeMatchView({ resume, onStatusChange }: ResumeMatchViewProps) {
const [selectedJD, setSelectedJD] = useState<JobDescription | null>(null)
const [matchResult, setMatchResult] = useState<MatchResult | null>(null)
const [loading, setLoading] = useState(false)
const handleSelectJD = (jobDescription: JobDescription) => {
setSelectedJD(jobDescription)
setMatchResult(null)
}
const handleMatch = async () => {
if (!selectedJD) return
try {
setLoading(true)
const response = await fetch("/api/match", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resumeId: resume.id,
jobDescription: selectedJD.description,
}),
})
if (!response.ok) throw new Error("Failed to match resume")
const data = await response.json()
if (data.success && data.matchResults) {
const result = data.matchResults;
const processedResult: MatchResult = {
matchScore: result.overallMatch,
matchedSkills: result.skillsMatch.matchedSkills,
missingSkills: result.skillsMatch.missingSkills,
experienceMatch: result.experienceMatch,
educationMatch: result.educationMatch,
overallAssessment: result.overallAssessment,
recommendations: result.recommendations
};
setMatchResult(processedResult);
toast({
title: "Match Complete",
description: `Match score: ${processedResult.matchScore}%`,
});
} else {
throw new Error("Invalid response from match API");
}
} catch (error) {
console.error("Error matching resume:", error)
toast({
title: "Error",
description: "Failed to match resume against job description",
variant: "destructive",
})
} finally {
setLoading(false)
}
}
const getScoreColor = (score: number) => {
if (score >= 80) return "text-green-400"
if (score >= 60) return "text-yellow-400"
return "text-red-400"
}
const getProgressColor = (score: number) => {
if (score >= 80) return "bg-green-500"
if (score >= 60) return "bg-yellow-500"
return "bg-red-500"
}
return (
<div className="flex-1 bg-gray-900 overflow-hidden flex flex-col">
<div className="p-6 border-b border-gray-800">
<div className="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold text-white">Match Resume</h1>
<p className="text-gray-400">Match {resume.name}'s resume against a job description</p>
</div>
</div>
<div className="mt-6">
<JobDescriptionSelector onSelect={handleSelectJD} />
</div>
{selectedJD && (
<div className="mt-4 p-4 bg-gray-800 rounded-md">
<h2 className="text-lg font-semibold text-white">{selectedJD.title}</h2>
<p className="text-gray-400 mt-2 line-clamp-3">{selectedJD.description}</p>
<Button
onClick={handleMatch}
disabled={loading}
className="mt-4 bg-[#0099ff] hover:bg-[#0077cc] text-white"
>
{loading ? "Matching..." : "Match Resume"}
</Button>
</div>
)}
</div>
<ScrollArea className="flex-1 p-6">
{matchResult ? (
<div className="space-y-6 max-w-3xl">
<div>
<h2 className="text-lg font-semibold text-white mb-3">Match Results</h2>
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-gray-800 p-4 rounded-md">
<h3 className="text-sm font-medium text-gray-400">Overall Match</h3>
<div className="flex items-center mt-2">
<span className={`text-2xl font-bold ${getScoreColor(matchResult.matchScore)}`}>
{matchResult.matchScore}%
</span>
</div>
<Progress
value={matchResult.matchScore}
className="h-2 mt-2 bg-gray-700"
indicatorClassName={getProgressColor(matchResult.matchScore)}
/>
</div>
<div className="bg-gray-800 p-4 rounded-md">
<h3 className="text-sm font-medium text-gray-400">Experience Match</h3>
<div className="flex items-center mt-2">
<span className={`text-2xl font-bold ${getScoreColor(matchResult.experienceMatch)}`}>
{matchResult.experienceMatch}%
</span>
</div>
<Progress
value={matchResult.experienceMatch}
className="h-2 mt-2 bg-gray-700"
indicatorClassName={getProgressColor(matchResult.experienceMatch)}
/>
</div>
<div className="bg-gray-800 p-4 rounded-md">
<h3 className="text-sm font-medium text-gray-400">Education Match</h3>
<div className="flex items-center mt-2">
<span className={`text-2xl font-bold ${getScoreColor(matchResult.educationMatch)}`}>
{matchResult.educationMatch}%
</span>
</div>
<Progress
value={matchResult.educationMatch}
className="h-2 mt-2 bg-gray-700"
indicatorClassName={getProgressColor(matchResult.educationMatch)}
/>
</div>
</div>
<div className="bg-gray-800 p-4 rounded-md mb-6">
<h3 className="text-sm font-medium text-gray-400 mb-2">Overall Assessment</h3>
<p className="text-white">{matchResult.overallAssessment}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-800 p-4 rounded-md">
<h3 className="text-sm font-medium text-gray-400 mb-2 flex items-center">
<CheckCircle2 className="h-4 w-4 text-green-500 mr-1" />
Matched Skills
</h3>
<div className="flex flex-wrap gap-2">
{matchResult.matchedSkills.length > 0 ? (
matchResult.matchedSkills.map((skill) => (
<Badge key={skill} className="bg-green-900 text-green-300 border-green-700">
{skill}
</Badge>
))
) : (
<p className="text-gray-500">No matched skills found</p>
)}
</div>
</div>
<div className="bg-gray-800 p-4 rounded-md">
<h3 className="text-sm font-medium text-gray-400 mb-2 flex items-center">
<XCircle className="h-4 w-4 text-red-500 mr-1" />
Missing Skills
</h3>
<div className="flex flex-wrap gap-2">
{matchResult.missingSkills.length > 0 ? (
matchResult.missingSkills.map((skill) => (
<Badge key={skill} className="bg-red-900 text-red-300 border-red-700">
{skill}
</Badge>
))
) : (
<p className="text-gray-500">No missing skills found</p>
)}
</div>
</div>
</div>
</div>
<Separator className="bg-gray-800" />
<div>
<h2 className="text-lg font-semibold text-white mb-3 flex items-center">
<AlertCircle className="h-5 w-5 text-[#0099ff] mr-2" />
Recommendations
</h2>
<ul className="space-y-2">
{matchResult.recommendations.map((recommendation, index) => (
<li key={index} className="bg-gray-800 p-3 rounded-md text-gray-300">
{recommendation}
</li>
))}
</ul>
</div>
<div className="mt-6 flex justify-end">
{matchResult.matchScore >= 80 && (
<Button
onClick={() => onStatusChange("Shortlisted")}
className="bg-green-600 hover:bg-green-700 text-white"
>
Shortlist Candidate
</Button>
)}
{matchResult.matchScore >= 60 && matchResult.matchScore < 80 && (
<Button
onClick={() => onStatusChange("Reviewed")}
className="bg-purple-600 hover:bg-purple-700 text-white"
>
Mark as Reviewed
</Button>
)}
{matchResult.matchScore < 60 && (
<Button onClick={() => onStatusChange("Rejected")} className="bg-red-600 hover:bg-red-700 text-white">
Reject Candidate
</Button>
)}
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="bg-gray-800 p-8 rounded-lg max-w-md">
<AlertCircle className="h-12 w-12 text-[#0099ff] mx-auto mb-4" />
<h2 className="text-xl font-semibold text-white mb-2">No Match Results</h2>
<p className="text-gray-400">
Select a job description and click "Match Resume" to see how well this candidate matches the
requirements.
</p>
</div>
</div>
)}
</ScrollArea>
</div>
)
}