From 9ccaec0e7ceba8a7a211852d747669bef6d5f1e7 Mon Sep 17 00:00:00 2001 From: David Kifer Date: Wed, 22 Jul 2026 17:12:51 -0400 Subject: [PATCH] Add AI Resume Upload & Automatic Parsing feature --- .app.pid | 2 +- server.ts | 99 ++++++++++++++++++++++++++++++ src/components/ProfileSettings.tsx | 85 ++++++++++++++++++++++--- 3 files changed, 177 insertions(+), 9 deletions(-) diff --git a/.app.pid b/.app.pid index b2573f7..41fc841 100644 --- a/.app.pid +++ b/.app.pid @@ -1 +1 @@ -95217 +95660 diff --git a/server.ts b/server.ts index ce399a4..c748a66 100644 --- a/server.ts +++ b/server.ts @@ -223,6 +223,105 @@ Extract the following fields accurately and output JSON: } }); +// API 2B: Parse Master Resume Text / Document into User Profile +app.post("/api/gemini/parse-resume", async (req, res) => { + try { + const { resumeText, llmConfig } = req.body; + + if (!resumeText || typeof resumeText !== 'string' || resumeText.trim().length === 0) { + return res.status(400).json({ success: false, error: "Resume text content is required." }); + } + + const prompt = `Analyze the following resume text and extract all candidate information into structured JSON: + +Resume Content: +${resumeText} + +Extract the following fields accurately: +- fullName: Full Name of candidate +- email: Email address +- phone: Phone number +- location: City, State or location preference +- linkedinUrl: LinkedIn profile URL if found +- githubUrl: GitHub profile URL if found +- portfolioUrl: Personal website/portfolio URL if found +- summary: Professional summary or objective paragraph +- skills: Array of technical & soft skills +- experience: Array of work experience objects, each with: + - id: unique string + - title: Job title + - company: Company name + - period: Date range (e.g. 2022 - Present) + - bullets: Array of accomplishment bullet points +- education: Array of education objects, each with: + - degree: Degree title + - institution: School or University name + - year: Graduation year`; + + const schema = { + type: Type.OBJECT, + properties: { + fullName: { type: Type.STRING }, + email: { type: Type.STRING }, + phone: { type: Type.STRING }, + location: { type: Type.STRING }, + linkedinUrl: { type: Type.STRING }, + githubUrl: { type: Type.STRING }, + portfolioUrl: { type: Type.STRING }, + summary: { type: Type.STRING }, + skills: { + type: Type.ARRAY, + items: { type: Type.STRING } + }, + experience: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + id: { type: Type.STRING }, + title: { type: Type.STRING }, + company: { type: Type.STRING }, + period: { type: Type.STRING }, + bullets: { + type: Type.ARRAY, + items: { type: Type.STRING } + } + } + } + }, + education: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + degree: { type: Type.STRING }, + institution: { type: Type.STRING }, + year: { type: Type.STRING } + } + } + } + }, + required: ["fullName", "skills", "experience"] + }; + + const { text, providerUsed } = await generateTextWithLLMFallback(prompt, schema, llmConfig); + const parsedProfile = JSON.parse(text || "{}"); + + // Ensure IDs on experience items + if (Array.isArray(parsedProfile.experience)) { + parsedProfile.experience = parsedProfile.experience.map((exp: any, index: number) => ({ + ...exp, + id: exp.id || `exp_parsed_${Date.now()}_${index}` + })); + } + + res.json({ success: true, profile: parsedProfile, providerUsed }); + } catch (error: any) { + console.error("Error parsing resume:", error); + res.status(500).json({ success: false, error: error.message || "Failed to parse resume" }); + } +}); + // API 3: Tailor Resume & Calculate Match Score app.post("/api/gemini/tailor-resume", async (req, res) => { try { diff --git a/src/components/ProfileSettings.tsx b/src/components/ProfileSettings.tsx index 302826e..41251e8 100644 --- a/src/components/ProfileSettings.tsx +++ b/src/components/ProfileSettings.tsx @@ -99,6 +99,54 @@ export const ProfileSettings: React.FC = ({ handleUpdateField('skills', updated); }; + const [isParsingResume, setIsParsingResume] = useState(false); + const [parseError, setParseError] = useState(null); + + const handleResumeFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsParsingResume(true); + setParseError(null); + + try { + const text = await file.text(); + const res = await fetch('/api/gemini/parse-resume', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + resumeText: text, + llmConfig: profile.llmConfig + }) + }); + + const data = await res.json(); + if (data.success && data.profile) { + const parsed = data.profile; + setProfile(prev => ({ + ...prev, + fullName: parsed.fullName || prev.fullName, + email: parsed.email || prev.email, + phone: parsed.phone || prev.phone, + location: parsed.location || prev.location, + linkedinUrl: parsed.linkedinUrl || prev.linkedinUrl, + githubUrl: parsed.githubUrl || prev.githubUrl, + portfolioUrl: parsed.portfolioUrl || prev.portfolioUrl, + summary: parsed.summary || prev.summary, + skills: Array.isArray(parsed.skills) && parsed.skills.length > 0 ? parsed.skills : prev.skills, + experience: Array.isArray(parsed.experience) && parsed.experience.length > 0 ? parsed.experience : prev.experience, + education: Array.isArray(parsed.education) && parsed.education.length > 0 ? parsed.education : prev.education + })); + } else { + setParseError(data.error || 'Failed to parse resume content.'); + } + } catch (err: any) { + setParseError(`Failed to read/parse file: ${err.message}`); + } finally { + setIsParsingResume(false); + } + }; + const handleSave = async () => { setIsSaving(true); try { @@ -126,16 +174,37 @@ export const ProfileSettings: React.FC = ({

- +
+ + + +
+ {parseError && ( +
+ + {parseError} +
+ )} + {/* Main Profile Forms */}