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 && (
+
+ )}
+
{/* Main Profile Forms */}