Resume Builder added.
This commit is contained in:
parent
fbd8c97377
commit
9fb03b0dc1
@ -9,3 +9,4 @@ COLLEGE_SCORECARD_KEY = BlZ0tIdmXVGI4G8NxJ9e6dXEiGUfAfnQJyw8bumj
|
||||
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
||||
REACT_APP_ENV=production
|
||||
REACT_APP_OPENAI_API_KEY=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
|
||||
OPENAI_API_KEY=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
|
@ -8,17 +8,27 @@ import sqlite3 from 'sqlite3';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import multer from 'multer';
|
||||
import mammoth from 'mammoth';
|
||||
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 __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 PORT = process.env.PREMIUM_PORT || 5002;
|
||||
|
||||
|
||||
let db;
|
||||
const initDB = async () => {
|
||||
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)
|
||||
------------------------------------------------------------------ */
|
||||
|
2830
package-lock.json
generated
2830
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,12 +17,19 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"cra-template": "1.2.0",
|
||||
"docx": "^9.5.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"file-saver": "^2.0.5",
|
||||
"helmet": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.483.0",
|
||||
"mammoth": "^1.9.0",
|
||||
"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-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
45
src/App.js
45
src/App.js
@ -8,7 +8,7 @@ import {
|
||||
Link,
|
||||
} from 'react-router-dom';
|
||||
|
||||
// Import all components from the components folder
|
||||
// Import all components
|
||||
import PremiumRoute from './components/PremiumRoute.js';
|
||||
import SessionExpiredHandler from './components/SessionExpiredHandler.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 MultiScenarioView from './components/MultiScenarioView.js';
|
||||
|
||||
// 1) Import your ResumeRewrite component
|
||||
import ResumeRewrite from './components/ResumeRewrite.js'; // adjust the path if needed
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -31,15 +34,20 @@ function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// If you want Resume Optimizer to be considered a "premium path" too:
|
||||
const premiumPaths = [
|
||||
'/milestone-tracker',
|
||||
'/paywall',
|
||||
'/financial-profile',
|
||||
'/multi-scenario',
|
||||
'/premium-onboarding',
|
||||
'/resume-optimizer', // add it here if you want
|
||||
];
|
||||
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
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
@ -134,7 +142,7 @@ function App() {
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
{/* Premium sections (greyed out if not is_premium) */}
|
||||
{/* Premium sections (still checking only user?.is_premium for these) */}
|
||||
<li>
|
||||
{user?.is_premium ? (
|
||||
<Link
|
||||
@ -207,6 +215,25 @@ function App() {
|
||||
)}
|
||||
</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 */}
|
||||
<li>
|
||||
<button
|
||||
@ -221,8 +248,8 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* "Upgrade to Premium" button if not premium and on a free path */}
|
||||
{showPremiumCTA && isAuthenticated && !user?.is_premium && (
|
||||
{/* "Upgrade to Premium" button if not premium/pro and on a free path */}
|
||||
{showPremiumCTA && isAuthenticated && !canAccessPremium && (
|
||||
<button
|
||||
className="rounded bg-blue-600 px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
onClick={() => navigate('/paywall')}
|
||||
@ -291,6 +318,16 @@ function App() {
|
||||
</PremiumRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 4) The new Resume Optimizer route */}
|
||||
<Route
|
||||
path="/resume-optimizer"
|
||||
element={
|
||||
<PremiumRoute user={user}>
|
||||
<ResumeRewrite />
|
||||
</PremiumRoute>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
// src/components/CareerSelectDropdown.js
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const CareerSelectDropdown = ({ existingCareerPaths, selectedCareer, onChange, loading, authFetch }) => {
|
||||
const fetchMilestones = (careerPathId) => {
|
||||
|
@ -13,7 +13,6 @@ import {
|
||||
|
||||
import { CareerSuggestions } from './CareerSuggestions.js';
|
||||
import PopoutPanel from './PopoutPanel.js';
|
||||
import MilestoneTracker from './MilestoneTracker.js';
|
||||
import CareerSearch from './CareerSearch.js'; // <--- Import your new search
|
||||
import Chatbot from './Chatbot.js';
|
||||
|
||||
@ -150,7 +149,7 @@ function Dashboard() {
|
||||
|
||||
// Show session expired modal
|
||||
const [showSessionExpiredModal, setShowSessionExpiredModal] = useState(false);
|
||||
const [sessionHandled, setSessionHandled] = useState(false);
|
||||
|
||||
|
||||
// ============= NEW State =============
|
||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||
|
@ -490,7 +490,6 @@ export default function MilestoneTimeline({
|
||||
// 9) Render
|
||||
// ------------------------------------------------------------------
|
||||
// Combine "Career" + "Financial" if you want them in a single list:
|
||||
const allMilestones = [...milestones.Career, ...milestones.Financial];
|
||||
|
||||
return (
|
||||
<div className="milestone-timeline" style={{ padding: '1rem' }}>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ClipLoader } from "react-spinners";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import LoanRepayment from "./LoanRepayment.js";
|
||||
import "./PopoutPanel.css"; // You can keep or remove depending on your needs
|
||||
|
||||
|
@ -7,12 +7,14 @@ function PremiumRoute({ user, children }) {
|
||||
return <Navigate to="/signin" replace />;
|
||||
}
|
||||
|
||||
if (!user.is_premium) {
|
||||
// Logged in but not premium; go to paywall
|
||||
// Check if user has *either* premium or pro
|
||||
const hasPremiumOrPro = user.is_premium || user.is_pro_premium;
|
||||
if (!hasPremiumOrPro) {
|
||||
// Logged in but neither plan; go to paywall
|
||||
return <Navigate to="/paywall" replace />;
|
||||
}
|
||||
|
||||
// User is logged in and has premium
|
||||
// User is logged in and has premium or pro
|
||||
return children;
|
||||
}
|
||||
|
||||
|
140
src/components/ResumeRewrite.js
Normal file
140
src/components/ResumeRewrite.js
Normal 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;
|
BIN
uploads/956f53dfb89ce1ad0f42f9262cd79ca3
Normal file
BIN
uploads/956f53dfb89ce1ad0f42f9262cd79ca3
Normal file
Binary file not shown.
BIN
uploads/fc674b7af2f80e12e5a618b0a67dcbb4
Normal file
BIN
uploads/fc674b7af2f80e12e5a618b0a67dcbb4
Normal file
Binary file not shown.
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user