Resume Builder added.

This commit is contained in:
Josh 2025-05-08 17:23:19 +00:00
parent fbd8c97377
commit 9fb03b0dc1
14 changed files with 1852 additions and 1335 deletions

View File

@ -9,3 +9,4 @@ COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
REACT_APP_API_URL=https://dev1.aptivaai.com/api REACT_APP_API_URL=https://dev1.aptivaai.com/api
REACT_APP_ENV=production REACT_APP_ENV=production
REACT_APP_OPENAI_API_KEY=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 REACT_APP_OPENAI_API_KEY=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
OPENAI_API_KEY=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

View File

@ -8,17 +8,27 @@ import sqlite3 from 'sqlite3';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import path from 'path'; import path from 'path';
import fs from 'fs';
import multer from 'multer';
import mammoth from 'mammoth';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
import OpenAI from 'openai';
// --- Basic file init ---
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '..', '.env') }); const rootPath = path.resolve(__dirname, '..'); // Up one level
const env = process.env.NODE_ENV?.trim() || 'development';
const envPath = path.resolve(rootPath, `.env.${env}`);
dotenv.config({ path: envPath }); // Load .env file
const app = express(); const app = express();
const PORT = process.env.PREMIUM_PORT || 5002; const PORT = process.env.PREMIUM_PORT || 5002;
let db; let db;
const initDB = async () => { const initDB = async () => {
try { try {
@ -1607,6 +1617,135 @@ app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser,
} }
}); });
/* ------------------------------------------------------------------
RESUME OPTIMIZATION ENDPOINT
------------------------------------------------------------------ */
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Setup file upload via multer
const upload = multer({ dest: 'uploads/' });
// Basic usage gating config
const MAX_MONTHLY_REWRITES_PREMIUM = 2;
// Helper: build GPT prompt
const buildResumePrompt = (resumeText, jobTitle, jobDescription) => `
You are an expert resume writer specialized in precisely tailoring existing resumes for optimal ATS compatibility and explicit alignment with provided job descriptions.
STRICT GUIDELINES:
1. DO NOT invent any new job titles, employers, dates, locations, compensation details, or roles not explicitly stated in the user's original resume.
2. Creatively but realistically reframe, reposition, and explicitly recontextualize the user's existing professional experiences and skills to clearly demonstrate alignment with the provided job description.
3. Emphasize transferable skills, tasks, and responsibilities from the user's provided resume content that directly match the requirements and responsibilities listed in the job description.
4. Clearly and explicitly incorporate exact keywords, responsibilities, skills, and competencies directly from the provided job description.
5. Minimize or entirely remove irrelevant technical jargon or specific software names not directly aligned with the job description.
6. Avoid generic résumé clichés (e.g., "results-driven," "experienced professional," "dedicated leader," "dynamic professional," etc.).
7. NEVER directly reuse specific details such as salary information, compensation, or other company-specific information from the provided job description.
Target Job Title:
${jobTitle}
Provided Job Description:
${jobDescription}
User's Original Resume:
${resumeText}
Precisely Tailored, ATS-Optimized Resume:
`;
app.post(
'/api/premium/resume/optimize',
upload.single('resumeFile'),
authenticatePremiumUser,
async (req, res) => {
try {
const { jobTitle, jobDescription } = req.body;
if (!jobTitle || !jobDescription || !req.file) {
return res.status(400).json({ error: 'Missing required fields.' });
}
const userId = req.userId;
const usageMonth = new Date().toISOString().slice(0, 7);
const userPlanRow = await db.get(`
SELECT is_premium, is_pro_premium
FROM user_profile
WHERE user_id = ?
`, [userId]);
let userPlan = 'basic'; // default
if (userPlanRow?.is_pro_premium) userPlan = 'pro';
else if (userPlanRow?.is_premium) userPlan = 'premium';
if (userPlan === 'premium') {
const usageRow = await db.get(
`SELECT usage_count FROM feature_usage
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
[userId, usageMonth]
);
const usageCount = usageRow?.usage_count || 0;
if (usageCount >= MAX_MONTHLY_REWRITES_PREMIUM) {
return res.status(403).json({ error: 'Monthly limit reached. Upgrade to Pro.' });
}
}
const filePath = req.file.path;
const fileExt = req.file.originalname.split('.').pop().toLowerCase();
let resumeText = '';
if (fileExt === 'pdf') {
resumeText = await extractTextFromPDF(filePath);
} else if (fileExt === 'docx') {
const result = await mammoth.extractRawText({ path: filePath });
resumeText = result.value;
} else {
return res.status(400).json({ error: 'Unsupported file type.' });
}
const prompt = buildResumePrompt(resumeText, jobTitle, jobDescription);
const completion = await openai.chat.completions.create({
model: 'gpt-4-turbo',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
});
const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || '';
if (userPlan === 'premium') {
const existing = await db.get(
`SELECT usage_count FROM feature_usage
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
[userId, usageMonth]
);
if (existing) {
await db.run(
`UPDATE feature_usage SET usage_count = usage_count + 1
WHERE user_id = ? AND feature_name = 'resume_optimize' AND usage_month = ?`,
[userId, usageMonth]
);
} else {
await db.run(
`INSERT INTO feature_usage (user_id, feature_name, usage_month, usage_count)
VALUES (?, 'resume_optimize', ?, 1)`,
[userId, usageMonth]
);
}
}
fs.unlinkSync(filePath);
res.json({ optimizedResume });
} catch (err) {
console.error('Error optimizing resume:', err);
res.status(500).json({ error: 'Failed to optimize resume.' });
}
}
);
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
FALLBACK (404 for unmatched routes) FALLBACK (404 for unmatched routes)
------------------------------------------------------------------ */ ------------------------------------------------------------------ */

2832
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,12 +17,19 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cra-template": "1.2.0", "cra-template": "1.2.0",
"docx": "^9.5.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"file-saver": "^2.0.5",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lucide-react": "^0.483.0", "lucide-react": "^0.483.0",
"mammoth": "^1.9.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"multer": "^1.4.5-lts.2",
"openai": "^4.97.0",
"pdf-parse": "^1.1.1",
"pdfjs-dist": "^3.11.174",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -8,7 +8,7 @@ import {
Link, Link,
} from 'react-router-dom'; } from 'react-router-dom';
// Import all components from the components folder // Import all components
import PremiumRoute from './components/PremiumRoute.js'; import PremiumRoute from './components/PremiumRoute.js';
import SessionExpiredHandler from './components/SessionExpiredHandler.js'; import SessionExpiredHandler from './components/SessionExpiredHandler.js';
import GettingStarted from './components/GettingStarted.js'; import GettingStarted from './components/GettingStarted.js';
@ -23,6 +23,9 @@ import Paywall from './components/Paywall.js';
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js'; import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
import MultiScenarioView from './components/MultiScenarioView.js'; import MultiScenarioView from './components/MultiScenarioView.js';
// 1) Import your ResumeRewrite component
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed
function App() { function App() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -31,15 +34,20 @@ function App() {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// If you want Resume Optimizer to be considered a "premium path" too:
const premiumPaths = [ const premiumPaths = [
'/milestone-tracker', '/milestone-tracker',
'/paywall', '/paywall',
'/financial-profile', '/financial-profile',
'/multi-scenario', '/multi-scenario',
'/premium-onboarding', '/premium-onboarding',
'/resume-optimizer', // add it here if you want
]; ];
const showPremiumCTA = !premiumPaths.includes(location.pathname); const showPremiumCTA = !premiumPaths.includes(location.pathname);
// 2) We'll define "canAccessPremium" to handle *both* is_premium or is_pro_premium
const canAccessPremium = user?.is_premium || user?.is_pro_premium;
// Rehydrate user if there's a token // Rehydrate user if there's a token
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@ -134,7 +142,7 @@ function App() {
</Link> </Link>
</li> </li>
{/* Premium sections (greyed out if not is_premium) */} {/* Premium sections (still checking only user?.is_premium for these) */}
<li> <li>
{user?.is_premium ? ( {user?.is_premium ? (
<Link <Link
@ -207,6 +215,25 @@ function App() {
)} )}
</li> </li>
{/* 3) A new link for Resume Optimizer that checks canAccessPremium */}
<li>
{canAccessPremium ? (
<Link
className="text-blue-600 hover:text-blue-800"
to="/resume-optimizer"
>
Resume Optimizer
</Link>
) : (
<span className="text-gray-400 cursor-not-allowed">
Resume Optimizer{' '}
<span className="text-green-600">
(Premium or Pro Only)
</span>
</span>
)}
</li>
{/* Logout */} {/* Logout */}
<li> <li>
<button <button
@ -221,8 +248,8 @@ function App() {
)} )}
</div> </div>
{/* "Upgrade to Premium" button if not premium and on a free path */} {/* "Upgrade to Premium" button if not premium/pro and on a free path */}
{showPremiumCTA && isAuthenticated && !user?.is_premium && ( {showPremiumCTA && isAuthenticated && !canAccessPremium && (
<button <button
className="rounded bg-blue-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700" className="rounded bg-blue-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
onClick={() => navigate('/paywall')} onClick={() => navigate('/paywall')}
@ -291,6 +318,16 @@ function App() {
</PremiumRoute> </PremiumRoute>
} }
/> />
{/* 4) The new Resume Optimizer route */}
<Route
path="/resume-optimizer"
element={
<PremiumRoute user={user}>
<ResumeRewrite />
</PremiumRoute>
}
/>
</> </>
)} )}

View File

@ -1,5 +1,5 @@
// src/components/CareerSelectDropdown.js // src/components/CareerSelectDropdown.js
import React, { useEffect } from 'react'; import React from 'react';
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => { const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => {
const fetchMilestones = (careerPathId) => { const fetchMilestones = (careerPathId) => {

View File

@ -13,7 +13,6 @@ import {
import { CareerSuggestions } from './CareerSuggestions.js'; import { CareerSuggestions } from './CareerSuggestions.js';
import PopoutPanel from './PopoutPanel.js'; import PopoutPanel from './PopoutPanel.js';
import MilestoneTracker from './MilestoneTracker.js';
import CareerSearch from './CareerSearch.js'; // <--- Import your new search import CareerSearch from './CareerSearch.js'; // <--- Import your new search
import Chatbot from './Chatbot.js'; import Chatbot from './Chatbot.js';
@ -150,7 +149,7 @@ function Dashboard() {
// Show session expired modal // Show session expired modal
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false); const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
const [sessionHandled, setSessionHandled] = useState(false);
// ============= NEW State ============= // ============= NEW State =============
const [pendingCareerForModal, setPendingCareerForModal] = useState(null); const [pendingCareerForModal, setPendingCareerForModal] = useState(null);

View File

@ -490,7 +490,6 @@ export default function MilestoneTimeline({
// 9) Render // 9) Render
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Combine "Career" + "Financial" if you want them in a single list: // Combine "Career" + "Financial" if you want them in a single list:
const allMilestones = [...milestones.Career, ...milestones.Financial];
return ( return (
<div className="milestone-timeline" style={{ padding: '1rem' }}> <div className="milestone-timeline" style={{ padding: '1rem' }}>

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ClipLoader } from "react-spinners"; import { ClipLoader } from "react-spinners";
import { v4 as uuidv4 } from "uuid";
import LoanRepayment from "./LoanRepayment.js"; import LoanRepayment from "./LoanRepayment.js";
import "./PopoutPanel.css"; // You can keep or remove depending on your needs import "./PopoutPanel.css"; // You can keep or remove depending on your needs

View File

@ -7,12 +7,14 @@ function PremiumRoute({ user, children }) {
return <Navigate to="/signin" replace />; return <Navigate to="/signin" replace />;
} }
if (!user.is_premium) { // Check if user has *either* premium or pro
// Logged in but not premium; go to paywall const hasPremiumOrPro = user.is_premium || user.is_pro_premium;
if (!hasPremiumOrPro) {
// Logged in but neither plan; go to paywall
return <Navigate to="/paywall" replace />; return <Navigate to="/paywall" replace />;
} }
// User is logged in and has premium // User is logged in and has premium or pro
return children; return children;
} }

View File

@ -0,0 +1,140 @@
import React, { useState } from 'react';
import axios from 'axios';
// If you still want file-saver + docx, import them here
function ResumeRewrite() {
const [resumeFile, setResumeFile] = useState(null);
const [jobTitle, setJobTitle] = useState('');
const [jobDescription, setJobDescription] = useState('');
const [optimizedResume, setOptimizedResume] = useState('');
const [error, setError] = useState('');
const handleFileChange = (e) => {
setResumeFile(e.target.files[0]);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!resumeFile || !jobTitle.trim() || !jobDescription.trim()) {
setError('Please fill in all fields.');
return;
}
try {
const token = localStorage.getItem('token');
const formData = new FormData();
formData.append('resumeFile', resumeFile);
formData.append('jobTitle', jobTitle);
formData.append('jobDescription', jobDescription);
const res = await axios.post('/api/premium/resume/optimize', formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
});
setOptimizedResume(res.data.optimizedResume || '');
setError('');
} catch (err) {
console.error('Resume optimization error:', err);
setError(err.response?.data?.error || 'Failed to optimize resume.');
}
};
// Optional: Download as docx
// const handleDownloadDocx = () => { ... }
return (
<div className="max-w-4xl mx-auto mt-8 p-6 bg-white rounded shadow">
<h2 className="text-2xl font-bold mb-4">Resume Optimizer</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* File Upload */}
<div>
<label className="block font-medium text-gray-700 mb-1">
Upload Resume (PDF or DOCX):
</label>
<input
type="file"
accept=".pdf,.docx"
onChange={handleFileChange}
className="file:mr-4 file:py-2 file:px-4
file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100
cursor-pointer
text-gray-600"
/>
</div>
{/* Job Title */}
<div>
<label className="block font-medium text-gray-700 mb-1">
Job Title:
</label>
<input
type="text"
value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)}
className="w-full border rounded px-3 py-2 focus:outline-none
focus:ring focus:ring-blue-200"
placeholder="e.g., Software Engineer"
/>
</div>
{/* Job Description */}
<div>
<label className="block font-medium text-gray-700 mb-1">
Job Description:
</label>
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
rows={4}
className="w-full border rounded px-3 py-2 focus:outline-none
focus:ring focus:ring-blue-200"
placeholder="Paste the job listing or requirements here..."
/>
</div>
{error && (
<p className="text-red-600 font-semibold">
{error}
</p>
)}
<button
type="submit"
className="inline-block bg-blue-600 text-white font-semibold
px-5 py-2 rounded hover:bg-blue-700
transition-colors"
>
Optimize Resume
</button>
</form>
{/* Optimized Resume Display */}
{optimizedResume && (
<div className="mt-8">
<h3 className="text-xl font-bold mb-2">Optimized Resume</h3>
<pre className="whitespace-pre-wrap bg-gray-50 p-4 rounded border">
{optimizedResume}
</pre>
{/*
Optional Download Button
<button
onClick={handleDownloadDocx}
className="mt-2 inline-block bg-green-600 text-white font-semibold
px-4 py-2 rounded hover:bg-green-700 transition-colors"
>
Download as DOCX
</button>
*/}
</div>
)}
</div>
);
}
export default ResumeRewrite;

Binary file not shown.

Binary file not shown.

Binary file not shown.